robotframework-okw-env-docker 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ from .docker_provider import DockerProvider
2
+ from .library import OkwEnvDockerLibrary
3
+
4
+ __all__ = ["DockerProvider", "OkwEnvDockerLibrary"]
@@ -0,0 +1,117 @@
1
+ """Docker provider for OKW environments.
2
+
3
+ Uses the Docker Python SDK (docker-py) to communicate directly with the
4
+ Docker Engine API. No local Docker CLI installation required.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import uuid
11
+
12
+ import docker
13
+
14
+ from okw_env.provider_base import OkwEnvProviderBase
15
+
16
+
17
+ class DockerProvider(OkwEnvProviderBase):
18
+ """Provisions environment components as Docker containers via the Engine API."""
19
+
20
+ def __init__(self, base_url: str | None = None, component: dict | None = None):
21
+ component = component or {}
22
+ self._base_url = (
23
+ base_url
24
+ or component.get("docker_host")
25
+ or os.environ.get("DOCKER_HOST", "unix:///var/run/docker.sock")
26
+ )
27
+ self._client: docker.DockerClient | None = None
28
+ self._containers: dict[str, docker.models.containers.Container] = {}
29
+
30
+ def _get_client(self) -> docker.DockerClient:
31
+ if self._client is None:
32
+ self._client = docker.DockerClient(base_url=self._base_url)
33
+ return self._client
34
+
35
+ def create(self, component: dict) -> str:
36
+ """Pull image and create a container (without starting)."""
37
+ image = component.get("image", "")
38
+ version = component.get("version", "latest")
39
+ full_image = f"{image}:{version}"
40
+ port = component.get("port")
41
+ env = component.get("env", {})
42
+ healthcheck_cmd = component.get("healthcheck")
43
+ name = f"okw-{uuid.uuid4().hex[:8]}"
44
+
45
+ self._get_client().images.pull(image, tag=version)
46
+
47
+ ports = {f"{port}/tcp": None} if port else {}
48
+ env_list = [f"{k}={v}" for k, v in env.items()]
49
+
50
+ healthcheck = None
51
+ if healthcheck_cmd:
52
+ healthcheck = docker.types.Healthcheck(
53
+ test=["CMD-SHELL", healthcheck_cmd],
54
+ interval=5_000_000_000,
55
+ timeout=3_000_000_000,
56
+ retries=10,
57
+ )
58
+
59
+ container = self._get_client().containers.create(
60
+ full_image,
61
+ name=name,
62
+ ports=ports,
63
+ environment=env_list,
64
+ healthcheck=healthcheck,
65
+ detach=True,
66
+ )
67
+ self._containers[name] = container
68
+ return name
69
+
70
+ def start(self, component_id: str) -> None:
71
+ container = self._require_container(component_id)
72
+ container.start()
73
+
74
+ def get_runtime_info(self, component_id: str) -> dict:
75
+ """Read the actual host port assigned by Docker after start."""
76
+ container = self._require_container(component_id)
77
+ container.reload()
78
+ ports = container.attrs.get("NetworkSettings", {}).get("Ports", {})
79
+ for container_port, bindings in ports.items():
80
+ if bindings:
81
+ return {"port": int(bindings[0]["HostPort"])}
82
+ return {}
83
+
84
+ def is_ready(self, component_id: str) -> bool:
85
+ container = self._require_container(component_id)
86
+ container.reload()
87
+ state = container.attrs.get("State", {})
88
+
89
+ health = state.get("Health", {})
90
+ if health:
91
+ return health.get("Status") == "healthy"
92
+
93
+ return state.get("Status") == "running"
94
+
95
+ def get_logs(self, component_id: str) -> str:
96
+ container = self._require_container(component_id)
97
+ return container.logs().decode("utf-8", errors="replace")
98
+
99
+ def snapshot(self, component_id: str) -> str:
100
+ container = self._require_container(component_id)
101
+ snapshot_tag = f"{component_id}-snapshot"
102
+ container.commit(repository=snapshot_tag)
103
+ return snapshot_tag
104
+
105
+ def stop(self, component_id: str) -> None:
106
+ container = self._require_container(component_id)
107
+ container.stop(timeout=10)
108
+
109
+ def destroy(self, component_id: str) -> None:
110
+ container = self._require_container(component_id)
111
+ container.remove(v=True, force=True)
112
+ del self._containers[component_id]
113
+
114
+ def _require_container(self, component_id: str):
115
+ if component_id not in self._containers:
116
+ raise KeyError(f"Unknown container '{component_id}'. Active: {list(self._containers.keys())}")
117
+ return self._containers[component_id]
@@ -0,0 +1,139 @@
1
+ """OKW Environment Docker Library – Robot Framework keywords for Docker-based environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from robot.api.deco import library
6
+
7
+ from okw_env.library import OkwEnvLibrary
8
+
9
+ from .docker_provider import DockerProvider
10
+
11
+
12
+ @library(scope="GLOBAL")
13
+ class OkwEnvDockerLibrary(OkwEnvLibrary):
14
+ """OKW Environment provisioning for Docker.
15
+
16
+ = Zero-Install Architecture =
17
+
18
+ This library communicates with the Docker Engine API directly via
19
+ the [https://docker-py.readthedocs.io/|Docker SDK for Python].
20
+ **No Docker CLI, no Docker Desktop, no docker-compose installation
21
+ required** — only ``pip install robotframework-okw-env-docker``
22
+ and a reachable Docker host.
23
+
24
+ This dramatically reduces installation NOISE: every machine that can
25
+ run Python and reach a Docker host over the network can provision
26
+ test environments — no admin rights, no platform-specific installers.
27
+
28
+ = Connection =
29
+
30
+ The Docker host is configured **per component in the YAML file** via
31
+ the ``docker_host`` key. This is intentional: the connection target is
32
+ a property of the test environment, not of the test machine. A test
33
+ suite can mix components on different Docker hosts — or even different
34
+ providers (Docker + Proxmox) — without any local configuration.
35
+
36
+ Resolution order for ``docker_host``:
37
+
38
+ | 1. | ``docker_host`` key in component YAML (recommended) |
39
+ | 2. | ``docker_host`` parameter on the Library constructor (fallback) |
40
+ | 3. | Environment variable ``DOCKER_HOST`` |
41
+ | 4. | Default: ``unix:///var/run/docker.sock`` (Linux only) |
42
+
43
+ *Why YAML and not a Library parameter?*
44
+
45
+ The test machine (Windows laptop, Linux CI runner) should not determine
46
+ where Docker runs. A developer on Windows and a CI runner on Linux use
47
+ the same YAML — the connection target stays the same. No ``DOCKER_HOST``
48
+ environment variable, no platform-specific config, no surprises.
49
+
50
+ = Installation =
51
+
52
+ | ``pip install robotframework-okw-env-docker`` |
53
+
54
+ That's it. One ``pip install``, one IP address in the YAML, test environments ready.
55
+
56
+ = Usage =
57
+
58
+ | **Settings** |
59
+ | Library | OkwEnvDockerLibrary | components_dir=components |
60
+
61
+ | **Test Cases** |
62
+ | Webshop mit Datenbank |
63
+ | | ENV_Start | PostgresDB |
64
+ | | ENV_BuildAndRun | |
65
+ | | ENV_WaitForReady | PostgresDB |
66
+ | | # ... test steps ... | |
67
+ | | [Teardown] | Run Keywords | ENV_SnapshotOnFail | ENV_Stop |
68
+
69
+ = Component YAML =
70
+
71
+ Each environment component is defined in a YAML file.
72
+ The ``docker_host`` tells the provider where to connect:
73
+
74
+ | ``PostgresDB.yaml`` |
75
+ | provider: docker |
76
+ | docker_host: tcp://192.168.1.123:2375 |
77
+ | image: postgres |
78
+ | version: "17" |
79
+ | port: 5432 |
80
+ | env: |
81
+ | POSTGRES_DB: testdb |
82
+ | healthcheck: "pg_isready -U postgres" |
83
+ | timeout: 30s |
84
+
85
+ = Environment Variables via $MEM{} =
86
+
87
+ After ``ENV_BuildAndRun``, **all YAML keys** are published as Robot
88
+ Framework test variables using dot-notation. This bridges environment
89
+ provisioning with test keywords — the test knows where to connect
90
+ without hardcoding anything.
91
+
92
+ | *Variable* | *Source* | *Example value* |
93
+ | ``${PostgresDB.image}`` | YAML key | ``postgres`` |
94
+ | ``${PostgresDB.version}`` | YAML key | ``17`` |
95
+ | ``${PostgresDB.port}`` | YAML key | ``5432`` |
96
+ | ``${PostgresDB.env.POSTGRES_DB}`` | YAML nested key | ``testdb`` |
97
+ | ``${PostgresDB.env.POSTGRES_PASSWORD}`` | YAML nested key | ``secret`` |
98
+ | ``${PostgresDB.docker_host}`` | YAML key | ``tcp://192.168.1.123:2375`` |
99
+ | ``${PostgresDB.ID}`` | Runtime (container ID) | ``okw-3df2dcc8`` |
100
+
101
+ Usage in test:
102
+
103
+ | ENV_Start | PostgresDB |
104
+ | ENV_BuildAndRun | |
105
+ | ENV_WaitForReady | PostgresDB |
106
+ | Log | DB name: ${PostgresDB.env.POSTGRES_DB} |
107
+ | Log | Port: ${PostgresDB.port} |
108
+
109
+ In OKW GUI/API keywords, use ``$MEM{}`` syntax:
110
+
111
+ | SetValue | DB_Host | $MEM{PostgresDB.docker_host} |
112
+ | SetValue | DB_Name | $MEM{PostgresDB.env.POSTGRES_DB} |
113
+
114
+ *Design rationale:* The connection data lives in the YAML — not in
115
+ environment variables, not in CI config, not hardcoded in the test.
116
+ The same test runs against any Docker host by changing one YAML file.
117
+
118
+ = Signal vs. NOISE =
119
+
120
+ | *Signal* | *NOISE (hidden)* |
121
+ | ``ENV_Start PostgresDB`` | Docker API calls, image pull, container config |
122
+ | ``ENV_WaitForReady PostgresDB`` | Health check polling, timeout logic |
123
+ | ``${PostgresDB.env.POSTGRES_DB}`` | YAML parsing, variable publishing |
124
+ | ``ENV_Stop`` | Container stop, remove, volume cleanup |
125
+
126
+ The test case reads like a specification. The infrastructure is invisible.
127
+
128
+ = Runnable Examples =
129
+
130
+ See [https://github.com/Hrabovszki1023/okw-examples|okw-examples]
131
+ for complete working test suites.
132
+ """
133
+
134
+ ROBOT_LIBRARY_SCOPE = "GLOBAL"
135
+
136
+ def __init__(self, components_dir: str | None = None, docker_host: str | None = None):
137
+ super().__init__(components_dir=components_dir)
138
+ provider = DockerProvider(base_url=docker_host)
139
+ self._manager.set_provider(provider)
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-okw-env-docker
3
+ Version: 0.1.0
4
+ Summary: Robot Framework library providing OKW environment provisioning with Docker.
5
+ Project-URL: Repository, http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env-docker
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: robotframework>=6.0
9
+ Requires-Dist: robotframework-okw-env>=0.1.0
10
+ Requires-Dist: docker>=7.0.0
11
+ Requires-Dist: pyyaml>=6.0
12
+
13
+ # robotframework-okw-env-docker
14
+
15
+ Docker Compose provider for [okw-env](http://192.168.1.130:3000/Hrabovszki1023/robotframework-okw-env).
16
+
17
+ > Deutsche Version: [README_de.md](README_de.md)
18
+
19
+ ## What
20
+
21
+ Implements the OKW Environment Provider Contract using Docker Compose.
22
+ Generates `docker-compose.yml` from YAML component definitions and
23
+ manages the full container lifecycle (create, start, health check,
24
+ logs, snapshot, stop, destroy).
25
+
26
+ ## Why — Signal vs. NOISE
27
+
28
+ Docker Compose syntax is NOISE. The tester only writes:
29
+
30
+ ```robot
31
+ ENV_Start PostgresDB
32
+ ENV_BuildAndRun
33
+ ENV_WaitForReady PostgresDB
34
+ ```
35
+
36
+ The framework generates the Compose file, starts the container,
37
+ and polls the health check. No Docker knowledge required in test code.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install robotframework-okw-env-docker
43
+ ```
44
+
45
+ This automatically installs `robotframework-okw-env` (core) and
46
+ `robotframework-docker` (Docker Compose engine).
47
+
48
+ ## Component YAML
49
+
50
+ ```yaml
51
+ PostgresDB:
52
+ provider: docker
53
+ image: postgres
54
+ version: "17"
55
+ port: 5432
56
+ env:
57
+ POSTGRES_DB: testdb
58
+ POSTGRES_PASSWORD: testpass
59
+ healthcheck: "pg_isready -U postgres"
60
+ timeout: 30s
61
+ ```
62
+
63
+ ## Minimal Example
64
+
65
+ ```robot
66
+ *** Settings ***
67
+ Library okw_env.library.OkwEnvLibrary components_dir=components WITH NAME ENV
68
+
69
+ *** Test Cases ***
70
+ Database Is Reachable
71
+ ENV_Start PostgresDB
72
+ ENV_BuildAndRun
73
+ ENV_WaitForReady PostgresDB
74
+ Log PostgresDB is ready for testing.
75
+ [Teardown] ENV_Stop
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ okw-env (keywords, YAML loader, registry)
82
+
83
+
84
+ okw-env-docker (this library)
85
+
86
+
87
+ DockerComposeLibrary (robotframework-docker)
88
+
89
+
90
+ Docker Compose CLI → Docker Engine
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ - [CONTRACT.md](docs/CONTRACT.md) — How this provider implements the contract
96
+ - [SPECIFICATION.md](docs/SPECIFICATION.md) — Lifecycle, isolation, snapshot model
97
+ - [KEYWORDS.md](docs/KEYWORDS.md) — Keyword reference with examples
@@ -0,0 +1,8 @@
1
+ okw_env_docker/__init__.py,sha256=o3N2xY3xSyxuqW9vhr9aK9ae_6OiUYHSGgTRiVEcu3w,138
2
+ okw_env_docker/docker_provider.py,sha256=6KmtQQFH0daQlkmVFGQT2Y6ldeNANAUHKPbSOJJz7D8,4176
3
+ okw_env_docker/library.py,sha256=wugiKI877qsirMJV45VPXkPG0UG-zBMc88Y-B91QAhk,5272
4
+ robotframework_okw_env_docker-0.1.0.dist-info/METADATA,sha256=R1rO0HybgIg_U9Ta31inU2-PtYvGHzG-tzmqCrxHIus,2429
5
+ robotframework_okw_env_docker-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ robotframework_okw_env_docker-0.1.0.dist-info/entry_points.txt,sha256=86OgurZYTD01naWHeROqyDHEcdGhn7tDmcz8RR-QFcI,59
7
+ robotframework_okw_env_docker-0.1.0.dist-info/top_level.txt,sha256=wESfcmQix8F2A2lBj8at_1yH07xXbnzvMpimNKykMXE,15
8
+ robotframework_okw_env_docker-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [okw_env.providers]
2
+ docker = okw_env_docker:DockerProvider
@@ -0,0 +1 @@
1
+ okw_env_docker