apx-dokploy 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.
- apx_dokploy/__init__.py +5 -0
- apx_dokploy/cli.py +38 -0
- apx_dokploy/client.py +144 -0
- apx_dokploy/overlays/cloud/dokploy/cookiecutter.json +14 -0
- apx_dokploy/overlays/cloud/dokploy/hooks/post_gen_project.py +9 -0
- apx_dokploy/overlays/cloud/dokploy/{{cookiecutter.project_slug}}/.dockerignore +11 -0
- apx_dokploy/overlays/cloud/dokploy/{{cookiecutter.project_slug}}/.github/workflows/docker-publish.yml +52 -0
- apx_dokploy/overlays/cloud/dokploy/{{cookiecutter.project_slug}}/DEPLOY.md +42 -0
- apx_dokploy/overlays/cloud/dokploy/{{cookiecutter.project_slug}}/Dockerfile +26 -0
- apx_dokploy/overlays/cloud/dokploy/{{cookiecutter.project_slug}}/docker-compose.yml +9 -0
- apx_dokploy/overlays/index.json +10 -0
- apx_dokploy/plugin.py +64 -0
- apx_dokploy/settings.py +46 -0
- apx_dokploy/target.py +555 -0
- apx_dokploy/tools.py +104 -0
- apx_dokploy-0.1.0.dist-info/METADATA +87 -0
- apx_dokploy-0.1.0.dist-info/RECORD +21 -0
- apx_dokploy-0.1.0.dist-info/WHEEL +5 -0
- apx_dokploy-0.1.0.dist-info/entry_points.txt +5 -0
- apx_dokploy-0.1.0.dist-info/licenses/LICENSE +201 -0
- apx_dokploy-0.1.0.dist-info/top_level.txt +1 -0
apx_dokploy/__init__.py
ADDED
apx_dokploy/cli.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""`action-platform dokploy …` — the same reads as the tools, from a terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from apx_dokploy.settings import connect
|
|
10
|
+
from apx_dokploy.tools import applications_of, deployments_of, projects_of
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
help="Dokploy: projects, applications, deployments.", no_args_is_help=True
|
|
14
|
+
)
|
|
15
|
+
options: Any | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("projects")
|
|
19
|
+
def projects() -> None:
|
|
20
|
+
"""List the projects and their environments."""
|
|
21
|
+
for p in projects_of(connect(options)):
|
|
22
|
+
typer.echo(f"{p.name}\t{', '.join(p.environments)}\t{p.id}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("applications")
|
|
26
|
+
def applications(project: str = typer.Option("", help="Only this project")) -> None:
|
|
27
|
+
"""List the applications, their status and image."""
|
|
28
|
+
for a in applications_of(connect(options), project):
|
|
29
|
+
typer.echo(
|
|
30
|
+
f"{a.project}/{a.environment}/{a.app_name}\t{a.status}\t{a.image or '-'}\t{a.id}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("deployments")
|
|
35
|
+
def deployments(application_id: str, limit: int = typer.Option(10)) -> None:
|
|
36
|
+
"""Deployment history of one application."""
|
|
37
|
+
for d in deployments_of(connect(options), application_id, limit):
|
|
38
|
+
typer.echo(f"{d.created_at}\t{d.status}\t{d.title or '-'}\t{d.id}")
|
apx_dokploy/client.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""The Dokploy REST API (`/api/<router>.<procedure>`, `x-api-key`) and the OCI registry the image lives in — plain urllib, no client library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib import error, parse, request
|
|
10
|
+
|
|
11
|
+
from action_platform.core.exception import DeployError
|
|
12
|
+
|
|
13
|
+
TIMEOUT = 30
|
|
14
|
+
MANIFEST_TYPES = ", ".join(
|
|
15
|
+
[
|
|
16
|
+
"application/vnd.oci.image.index.v1+json",
|
|
17
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
18
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
19
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApiError(DeployError):
|
|
25
|
+
def __init__(self, status: int, detail: str) -> None:
|
|
26
|
+
super().__init__(f"dokploy answered {status}: {detail}")
|
|
27
|
+
self.status = status
|
|
28
|
+
self.detail = detail
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Dokploy:
|
|
32
|
+
"""One Dokploy instance: `get` and `post` against its API with the key."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, url: str, api_key: str) -> None:
|
|
35
|
+
self.url = url.rstrip("/")
|
|
36
|
+
self.api_key = api_key
|
|
37
|
+
|
|
38
|
+
def get(self, procedure: str, **query: Any) -> Any:
|
|
39
|
+
target = f"{self.url}/api/{procedure}"
|
|
40
|
+
|
|
41
|
+
if query:
|
|
42
|
+
target += "?" + parse.urlencode(query)
|
|
43
|
+
|
|
44
|
+
return self._send("GET", target, None)
|
|
45
|
+
|
|
46
|
+
def post(self, procedure: str, body: dict[str, Any] | None = None) -> Any:
|
|
47
|
+
return self._send("POST", f"{self.url}/api/{procedure}", body or {})
|
|
48
|
+
|
|
49
|
+
def _send(self, method: str, target: str, body: dict[str, Any] | None) -> Any:
|
|
50
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
51
|
+
req = request.Request(target, data=data, method=method)
|
|
52
|
+
req.add_header("x-api-key", self.api_key)
|
|
53
|
+
req.add_header("accept", "application/json")
|
|
54
|
+
|
|
55
|
+
if data is not None:
|
|
56
|
+
req.add_header("content-type", "application/json")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
with request.urlopen(req, timeout=TIMEOUT) as response:
|
|
60
|
+
text = response.read().decode(errors="replace")
|
|
61
|
+
except error.HTTPError as e:
|
|
62
|
+
raise ApiError(e.code, e.read().decode(errors="replace")[:500]) from e
|
|
63
|
+
except (error.URLError, TimeoutError) as e:
|
|
64
|
+
raise DeployError(f"dokploy unreachable at {self.url}: {e}") from e
|
|
65
|
+
|
|
66
|
+
return json.loads(text) if text.strip() else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def image_exists(
|
|
70
|
+
image: str,
|
|
71
|
+
tag: str,
|
|
72
|
+
username: str | None = None,
|
|
73
|
+
password: str | None = None,
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""Whether `image:tag` is in its registry — a manifest HEAD, with the bearer token the registry asks for when it asks for one."""
|
|
76
|
+
registry, name = _split(image)
|
|
77
|
+
target = f"https://{registry}/v2/{name}/manifests/{tag}"
|
|
78
|
+
basic = (
|
|
79
|
+
base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
80
|
+
if username and password
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
83
|
+
status, challenge = _head(target, "Basic " + basic if basic else None)
|
|
84
|
+
|
|
85
|
+
if status == 401 and challenge:
|
|
86
|
+
token = _token(challenge, basic)
|
|
87
|
+
status, _ = _head(target, f"Bearer {token}")
|
|
88
|
+
|
|
89
|
+
if status == 200:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
if status == 404:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
raise DeployError(f"registry {registry} answered {status} for {name}:{tag}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _split(image: str) -> tuple[str, str]:
|
|
99
|
+
head, _, rest = image.partition("/")
|
|
100
|
+
|
|
101
|
+
if rest and ("." in head or ":" in head or head == "localhost"):
|
|
102
|
+
return head, rest
|
|
103
|
+
|
|
104
|
+
name = image if "/" in image else f"library/{image}"
|
|
105
|
+
|
|
106
|
+
return "registry-1.docker.io", name
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _head(target: str, authorization: str | None) -> tuple[int, str]:
|
|
110
|
+
req = request.Request(target, method="HEAD")
|
|
111
|
+
req.add_header("accept", MANIFEST_TYPES)
|
|
112
|
+
|
|
113
|
+
if authorization:
|
|
114
|
+
req.add_header("authorization", authorization)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with request.urlopen(req, timeout=TIMEOUT) as response:
|
|
118
|
+
return response.status, ""
|
|
119
|
+
except error.HTTPError as e:
|
|
120
|
+
return e.code, e.headers.get("www-authenticate", "")
|
|
121
|
+
except (error.URLError, TimeoutError) as e:
|
|
122
|
+
raise DeployError(f"registry unreachable: {e}") from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _token(challenge: str, basic: str | None) -> str:
|
|
126
|
+
fields = dict(re.findall(r'(\w+)="([^"]*)"', challenge))
|
|
127
|
+
realm = fields.get("realm")
|
|
128
|
+
|
|
129
|
+
if not realm:
|
|
130
|
+
raise DeployError(f"registry challenge not understood: {challenge}")
|
|
131
|
+
|
|
132
|
+
query = {k: v for k, v in fields.items() if k in ("service", "scope")}
|
|
133
|
+
req = request.Request(f"{realm}?{parse.urlencode(query)}")
|
|
134
|
+
|
|
135
|
+
if basic:
|
|
136
|
+
req.add_header("authorization", f"Basic {basic}")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
with request.urlopen(req, timeout=TIMEOUT) as response:
|
|
140
|
+
data = json.loads(response.read().decode())
|
|
141
|
+
except error.HTTPError as e:
|
|
142
|
+
raise DeployError(f"registry token refused ({e.code}): check the credentials")
|
|
143
|
+
|
|
144
|
+
return data.get("token") or data.get("access_token") or ""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "My Project",
|
|
3
|
+
"project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}",
|
|
4
|
+
"github_owner": "actionplatform",
|
|
5
|
+
"package_name": "{{ cookiecutter.project_slug|replace('-', '_') }}",
|
|
6
|
+
"language": ["python", "node", "go", "java", "kotlin", "ruby"],
|
|
7
|
+
"type": ["web"],
|
|
8
|
+
"ci": ["github", "gitlab", "jenkins"],
|
|
9
|
+
"_kind": "cloud",
|
|
10
|
+
"_cloud": "dokploy",
|
|
11
|
+
"_description": "Docker image published by the CI on every release, run by Dokploy",
|
|
12
|
+
"_types": ["web"],
|
|
13
|
+
"_copy_without_render": [".github/workflows/docker-publish.yml"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Package Docker
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
packages: write
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: image-${{ github.ref_name }}
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
name: Build and push ${{ github.ref_name }}
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Read the version from the tag
|
|
24
|
+
id: version
|
|
25
|
+
run: |
|
|
26
|
+
tag="${GITHUB_REF_NAME}"
|
|
27
|
+
version="${tag#v}"
|
|
28
|
+
[ "$version" = "$tag" ] && version="$(cat LAST_VERSION)"
|
|
29
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
30
|
+
echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
|
|
31
|
+
|
|
32
|
+
- uses: docker/setup-qemu-action@v3
|
|
33
|
+
- uses: docker/setup-buildx-action@v3
|
|
34
|
+
|
|
35
|
+
- uses: docker/login-action@v3
|
|
36
|
+
with:
|
|
37
|
+
registry: ghcr.io
|
|
38
|
+
username: ${{ github.actor }}
|
|
39
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
40
|
+
|
|
41
|
+
- uses: docker/build-push-action@v6
|
|
42
|
+
with:
|
|
43
|
+
context: .
|
|
44
|
+
platforms: linux/amd64,linux/arm64
|
|
45
|
+
push: true
|
|
46
|
+
tags: |
|
|
47
|
+
${{ steps.version.outputs.image }}:${{ steps.version.outputs.version }}
|
|
48
|
+
${{ steps.version.outputs.image }}:latest
|
|
49
|
+
cache-from: type=gha
|
|
50
|
+
cache-to: type=gha,mode=max
|
|
51
|
+
|
|
52
|
+
- run: echo "${{ steps.version.outputs.image }}:${{ steps.version.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Deploy — Dokploy
|
|
2
|
+
|
|
3
|
+
Overlay `cloud/dokploy`. The CI builds the image on every release and Dokploy pulls it; the platform tells Dokploy which tag to run.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Image | `ghcr.io/{{ cookiecutter.github_owner }}/{{ cookiecutter.project_slug }}:<version>` — `.github/workflows/docker-publish.yml`, on every published release |
|
|
8
|
+
| Dokploy project | `{{ cookiecutter.project_slug }}` |
|
|
9
|
+
| Dokploy environment | one per scope: `dev`, `prod` |
|
|
10
|
+
| Application | `{{ cookiecutter.project_slug }}-<scope>`, listens on port 8000 |
|
|
11
|
+
|
|
12
|
+
## Set up once
|
|
13
|
+
|
|
14
|
+
1. In Dokploy, Settings → API Keys: create a key. Put it and the instance url in the plugin's options (Plugins → Dokploy → Configure on the platform; `DOKPLOY_URL` and `DOKPLOY_API_KEY` on a machine).
|
|
15
|
+
2. Is the GitHub package private? Give the plugin a registry username and a token with `read:packages`, or make the package public under the repository's Packages settings.
|
|
16
|
+
3. `platform.toml`:
|
|
17
|
+
|
|
18
|
+
```toml
|
|
19
|
+
[deploy]
|
|
20
|
+
target = "dokploy"
|
|
21
|
+
|
|
22
|
+
[deploy.domains]
|
|
23
|
+
prod = "{{ cookiecutter.project_slug }}.example.com"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The first deploy creates the project, the environment, the application and the domain (Let's Encrypt). Point the DNS record at the Dokploy server before that.
|
|
27
|
+
|
|
28
|
+
## Every release
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
action-platform release minor # tags vX.Y.Z; the workflow publishes the image
|
|
32
|
+
action-platform deploy --dry-run # readiness: settings, image published, key accepted, application state
|
|
33
|
+
action-platform deploy # saveDockerProvider(<image>:<version>) + deploy, followed to done
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`rollback` redeploys the previous version from the application's deployment history; `diagnose` reports the status, the image and the url.
|
|
37
|
+
|
|
38
|
+
## Locally
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
docker compose up --build # http://localhost:8000
|
|
42
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Two stages, the same shape for every language: the build image assembles the app with ap-build,
|
|
2
|
+
# the runtime image runs it. The app serves HTTP on $PORT.
|
|
3
|
+
{%- set lang = "java" if cookiecutter.language in ["java", "kotlin"] else cookiecutter.language %}
|
|
4
|
+
FROM ghcr.io/actionplatform/build-{{ lang }}:0 AS build
|
|
5
|
+
ARG TARGETARCH
|
|
6
|
+
WORKDIR /w
|
|
7
|
+
COPY . .
|
|
8
|
+
RUN AP_ARCH=$([ "$TARGETARCH" = "amd64" ] && echo x86_64 || echo arm64) AP_ARTIFACTS=/out ap-build package
|
|
9
|
+
|
|
10
|
+
FROM ghcr.io/actionplatform/runtime-{{ lang }}:0
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY --from=build /out /app
|
|
13
|
+
ENV PORT=8000
|
|
14
|
+
EXPOSE 8000
|
|
15
|
+
{%- if cookiecutter.language == "python" %}
|
|
16
|
+
ENV PYTHONPATH=/app
|
|
17
|
+
CMD ["-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
18
|
+
{%- elif cookiecutter.language == "node" %}
|
|
19
|
+
CMD ["dist/server.js"]
|
|
20
|
+
{%- elif cookiecutter.language == "go" %}
|
|
21
|
+
ENTRYPOINT ["/app/bootstrap"]
|
|
22
|
+
{%- elif cookiecutter.language == "ruby" %}
|
|
23
|
+
CMD ["bundle", "exec", "rackup", "-s", "webrick", "-o", "0.0.0.0", "-p", "8000"]
|
|
24
|
+
{%- else %}
|
|
25
|
+
CMD ["-jar", "/app/app.jar", "--server.port=8000"]
|
|
26
|
+
{%- endif %}
|
apx_dokploy/plugin.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""The plugin: the dokploy overlay, `action-platform dokploy` commands, `dokploy_*` tools, and the options an organization fills in."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from action_platform.abc import Option, Plugin, Surface
|
|
8
|
+
|
|
9
|
+
from apx_dokploy import cli
|
|
10
|
+
from apx_dokploy.tools import register_tools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DokployPlugin(Plugin):
|
|
14
|
+
slug = "dokploy"
|
|
15
|
+
name = "Dokploy"
|
|
16
|
+
description = (
|
|
17
|
+
"Deploy a Docker image the CI publishes to a Dokploy instance; "
|
|
18
|
+
"read its projects, applications and deployments"
|
|
19
|
+
)
|
|
20
|
+
min_core = "0.28.1"
|
|
21
|
+
needs = [
|
|
22
|
+
"env: DOKPLOY_URL, DOKPLOY_API_KEY on a machine (the plugin's options on the platform)",
|
|
23
|
+
"net: the Dokploy instance's url; the image registry (ghcr.io by default)",
|
|
24
|
+
]
|
|
25
|
+
options = [
|
|
26
|
+
Option(
|
|
27
|
+
"url",
|
|
28
|
+
"Dokploy URL",
|
|
29
|
+
"url",
|
|
30
|
+
help="Where the Dokploy dashboard answers, https://dokploy.example.com. A project may override it with [deploy] url.",
|
|
31
|
+
required=True,
|
|
32
|
+
),
|
|
33
|
+
Option(
|
|
34
|
+
"api_key",
|
|
35
|
+
"API key",
|
|
36
|
+
"secret",
|
|
37
|
+
help="Generated under Settings > API Keys in Dokploy. Deploys, creates and deletes applications on your behalf; never written to a repository.",
|
|
38
|
+
required=True,
|
|
39
|
+
),
|
|
40
|
+
Option(
|
|
41
|
+
"registry_username",
|
|
42
|
+
"Registry username",
|
|
43
|
+
"text",
|
|
44
|
+
help="Only for a private image registry: the user Dokploy pulls with. Empty for a public image.",
|
|
45
|
+
),
|
|
46
|
+
Option(
|
|
47
|
+
"registry_password",
|
|
48
|
+
"Registry password",
|
|
49
|
+
"secret",
|
|
50
|
+
help="The token that goes with the registry username (a GitHub token with read:packages for ghcr.io).",
|
|
51
|
+
),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def overlays(self) -> Path:
|
|
56
|
+
return Path(__file__).parent / "overlays"
|
|
57
|
+
|
|
58
|
+
def register(self, surface: Surface) -> None:
|
|
59
|
+
if surface.mcp is not None:
|
|
60
|
+
register_tools(surface.mcp, surface.options)
|
|
61
|
+
|
|
62
|
+
if surface.cli is not None:
|
|
63
|
+
cli.options = surface.options
|
|
64
|
+
surface.cli.add_typer(cli.app, name="dokploy")
|
apx_dokploy/settings.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Where a value comes from, in order: the target's own argument, what the deploy job carries (`AP_DOKPLOY_<KEY>` in `ctx.env`), the process environment (`AP_DOKPLOY_<KEY>`, then the plain name such as `DOKPLOY_URL`), the plugin's options file on this machine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from action_platform.core.exception import ActionPlatformError
|
|
9
|
+
from action_platform.plugins.options import FileOptions
|
|
10
|
+
|
|
11
|
+
from apx_dokploy.client import Dokploy
|
|
12
|
+
|
|
13
|
+
SLUG = "dokploy"
|
|
14
|
+
PLAIN = {"url": "DOKPLOY_URL", "api_key": "DOKPLOY_API_KEY"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setting(
|
|
18
|
+
key: str,
|
|
19
|
+
explicit: str | None = None,
|
|
20
|
+
env: dict[str, str] | None = None,
|
|
21
|
+
options: Any | None = None,
|
|
22
|
+
) -> str | None:
|
|
23
|
+
name = f"AP_{SLUG}_{key}".upper()
|
|
24
|
+
stored = (options or FileOptions(SLUG)).get(key)
|
|
25
|
+
found = (
|
|
26
|
+
explicit
|
|
27
|
+
or (env or {}).get(name)
|
|
28
|
+
or os.environ.get(name)
|
|
29
|
+
or os.environ.get(PLAIN.get(key, ""))
|
|
30
|
+
or stored
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return str(found) if found else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def connect(options: Any | None = None) -> Dokploy:
|
|
37
|
+
url = setting("url", options=options)
|
|
38
|
+
key = setting("api_key", options=options)
|
|
39
|
+
|
|
40
|
+
if not url or not key:
|
|
41
|
+
raise ActionPlatformError(
|
|
42
|
+
"dokploy: set the plugin's url and api_key options on the platform, "
|
|
43
|
+
"or DOKPLOY_URL and DOKPLOY_API_KEY on a machine"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return Dokploy(url, key)
|