ptctools 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.
- ptctools/__init__.py +3 -0
- ptctools/_portainer.py +279 -0
- ptctools/_s3.py +150 -0
- ptctools/cli.py +28 -0
- ptctools/config.py +293 -0
- ptctools/db.py +544 -0
- ptctools/stack.py +367 -0
- ptctools/utils.py +416 -0
- ptctools/volume.py +359 -0
- ptctools-0.1.0.dist-info/METADATA +99 -0
- ptctools-0.1.0.dist-info/RECORD +14 -0
- ptctools-0.1.0.dist-info/WHEEL +4 -0
- ptctools-0.1.0.dist-info/entry_points.txt +2 -0
- ptctools-0.1.0.dist-info/licenses/LICENSE +201 -0
ptctools/config.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Docker Swarm Config management commands via Portainer API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from ptctools._portainer import api_request
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_configs(
|
|
17
|
+
portainer_url: str,
|
|
18
|
+
api_key: str,
|
|
19
|
+
endpoint_id: int,
|
|
20
|
+
) -> tuple[list | None, int]:
|
|
21
|
+
"""List all Docker configs."""
|
|
22
|
+
url = f"{portainer_url}/api/endpoints/{endpoint_id}/docker/configs"
|
|
23
|
+
return api_request(url, api_key)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_config(
|
|
27
|
+
portainer_url: str,
|
|
28
|
+
api_key: str,
|
|
29
|
+
endpoint_id: int,
|
|
30
|
+
config_id: str,
|
|
31
|
+
) -> tuple[dict | None, int]:
|
|
32
|
+
"""Get a specific Docker config by ID."""
|
|
33
|
+
url = f"{portainer_url}/api/endpoints/{endpoint_id}/docker/configs/{config_id}"
|
|
34
|
+
return api_request(url, api_key)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_config(
|
|
38
|
+
portainer_url: str,
|
|
39
|
+
api_key: str,
|
|
40
|
+
endpoint_id: int,
|
|
41
|
+
name: str,
|
|
42
|
+
data: str,
|
|
43
|
+
labels: dict | None = None,
|
|
44
|
+
) -> tuple[dict | None, int]:
|
|
45
|
+
"""Create a new Docker config."""
|
|
46
|
+
url = f"{portainer_url}/api/endpoints/{endpoint_id}/docker/configs/create"
|
|
47
|
+
|
|
48
|
+
# Docker API requires base64 encoded data
|
|
49
|
+
encoded_data = base64.b64encode(data.encode("utf-8")).decode("utf-8")
|
|
50
|
+
|
|
51
|
+
payload = {
|
|
52
|
+
"Name": name,
|
|
53
|
+
"Data": encoded_data,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if labels:
|
|
57
|
+
payload["Labels"] = labels
|
|
58
|
+
|
|
59
|
+
return api_request(url, api_key, method="POST", data=payload)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def delete_config(
|
|
63
|
+
portainer_url: str,
|
|
64
|
+
api_key: str,
|
|
65
|
+
endpoint_id: int,
|
|
66
|
+
config_id: str,
|
|
67
|
+
) -> tuple[dict | None, int]:
|
|
68
|
+
"""Delete a Docker config."""
|
|
69
|
+
url = f"{portainer_url}/api/endpoints/{endpoint_id}/docker/configs/{config_id}"
|
|
70
|
+
return api_request(url, api_key, method="DELETE")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def find_config_by_name(
|
|
74
|
+
portainer_url: str,
|
|
75
|
+
api_key: str,
|
|
76
|
+
endpoint_id: int,
|
|
77
|
+
name: str,
|
|
78
|
+
) -> dict | None:
|
|
79
|
+
"""Find a config by name and return its details."""
|
|
80
|
+
configs, status_code = list_configs(portainer_url, api_key, endpoint_id)
|
|
81
|
+
|
|
82
|
+
if status_code != 200 or not isinstance(configs, list):
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
for config in configs:
|
|
86
|
+
spec = config.get("Spec", {})
|
|
87
|
+
if spec.get("Name") == name:
|
|
88
|
+
return config
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@click.group()
|
|
94
|
+
def cli():
|
|
95
|
+
"""Docker Swarm Config management commands."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@cli.command("set")
|
|
100
|
+
@click.option("--url", "-u", required=True, help="Portainer base URL")
|
|
101
|
+
@click.option("--name", "-n", required=True, help="Config name")
|
|
102
|
+
@click.option("--data", "-d", default=None, help="Config data (string content)")
|
|
103
|
+
@click.option(
|
|
104
|
+
"--file",
|
|
105
|
+
"-f",
|
|
106
|
+
"file_path",
|
|
107
|
+
type=click.Path(exists=True, path_type=Path),
|
|
108
|
+
default=None,
|
|
109
|
+
help="Read config data from file",
|
|
110
|
+
)
|
|
111
|
+
@click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
|
|
112
|
+
@click.option(
|
|
113
|
+
"--force", is_flag=True, help="Replace existing config if it exists"
|
|
114
|
+
)
|
|
115
|
+
def set_config(
|
|
116
|
+
url: str,
|
|
117
|
+
name: str,
|
|
118
|
+
data: str | None,
|
|
119
|
+
file_path: Path | None,
|
|
120
|
+
endpoint_id: int,
|
|
121
|
+
force: bool,
|
|
122
|
+
):
|
|
123
|
+
"""Create or update a Docker Swarm config.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
|
|
127
|
+
# Create from inline data
|
|
128
|
+
ptctools config set -u https://portainer.example.com -n my-config -d "config content"
|
|
129
|
+
|
|
130
|
+
# Create from file
|
|
131
|
+
ptctools config set -u https://portainer.example.com -n nginx.conf -f ./nginx.conf
|
|
132
|
+
|
|
133
|
+
# Replace existing config
|
|
134
|
+
ptctools config set -u https://portainer.example.com -n my-config -d "new content" --force
|
|
135
|
+
"""
|
|
136
|
+
# Validate that exactly one of --data or --file is provided
|
|
137
|
+
if data is None and file_path is None:
|
|
138
|
+
click.echo("Error: Either --data or --file must be provided", err=True)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
if data is not None and file_path is not None:
|
|
141
|
+
click.echo("Error: Cannot use both --data and --file", err=True)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Read data from file if provided
|
|
145
|
+
if file_path is not None:
|
|
146
|
+
data = file_path.read_text()
|
|
147
|
+
|
|
148
|
+
access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
|
|
149
|
+
if not access_token:
|
|
150
|
+
click.echo(
|
|
151
|
+
"Error: Missing PORTAINER_ACCESS_TOKEN environment variable", err=True
|
|
152
|
+
)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
portainer_url = url.rstrip("/")
|
|
156
|
+
|
|
157
|
+
# Check if config already exists
|
|
158
|
+
existing = find_config_by_name(portainer_url, access_token, endpoint_id, name)
|
|
159
|
+
|
|
160
|
+
if existing:
|
|
161
|
+
if not force:
|
|
162
|
+
click.echo(
|
|
163
|
+
f"Error: Config '{name}' already exists. Use --force to replace.",
|
|
164
|
+
err=True,
|
|
165
|
+
)
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
168
|
+
# Delete existing config first (Docker doesn't support updating configs)
|
|
169
|
+
# Docker Swarm configs are immutable - no update API exists.
|
|
170
|
+
# The only way to change a config is to delete and recreate it.
|
|
171
|
+
click.echo(f"Removing existing config: {name}")
|
|
172
|
+
config_id = existing.get("ID")
|
|
173
|
+
_, status_code = delete_config(
|
|
174
|
+
portainer_url, access_token, endpoint_id, config_id
|
|
175
|
+
)
|
|
176
|
+
if status_code not in (200, 204):
|
|
177
|
+
click.echo(
|
|
178
|
+
f"Error: Failed to delete existing config (HTTP {status_code})",
|
|
179
|
+
err=True,
|
|
180
|
+
)
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
|
|
183
|
+
click.echo(f"Creating config: {name}")
|
|
184
|
+
response, status_code = create_config(
|
|
185
|
+
portainer_url, access_token, endpoint_id, name, data
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if 200 <= status_code < 300:
|
|
189
|
+
config_id = response.get("ID") if response else "unknown"
|
|
190
|
+
click.echo(f"✓ Config '{name}' created successfully (ID: {config_id})")
|
|
191
|
+
else:
|
|
192
|
+
click.echo(f"Error: Failed to create config (HTTP {status_code})", err=True)
|
|
193
|
+
if response:
|
|
194
|
+
click.echo(json.dumps(response, indent=2), err=True)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@cli.command("get")
|
|
199
|
+
@click.option("--url", "-u", required=True, help="Portainer base URL")
|
|
200
|
+
@click.option("--name", "-n", required=True, help="Config name")
|
|
201
|
+
@click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
|
|
202
|
+
def get_config_cmd(url: str, name: str, endpoint_id: int):
|
|
203
|
+
"""Get details of a Docker Swarm config by name.
|
|
204
|
+
|
|
205
|
+
Note: Docker API does not return the actual config data for security reasons.
|
|
206
|
+
"""
|
|
207
|
+
access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
|
|
208
|
+
if not access_token:
|
|
209
|
+
click.echo(
|
|
210
|
+
"Error: Missing PORTAINER_ACCESS_TOKEN environment variable", err=True
|
|
211
|
+
)
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
|
|
214
|
+
portainer_url = url.rstrip("/")
|
|
215
|
+
|
|
216
|
+
config = find_config_by_name(portainer_url, access_token, endpoint_id, name)
|
|
217
|
+
|
|
218
|
+
if config:
|
|
219
|
+
click.echo(json.dumps(config, indent=2))
|
|
220
|
+
else:
|
|
221
|
+
click.echo(f"Error: Config '{name}' not found", err=True)
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@cli.command("list")
|
|
226
|
+
@click.option("--url", "-u", required=True, help="Portainer base URL")
|
|
227
|
+
@click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
|
|
228
|
+
def list_configs_cmd(url: str, endpoint_id: int):
|
|
229
|
+
"""List all Docker Swarm configs."""
|
|
230
|
+
access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
|
|
231
|
+
if not access_token:
|
|
232
|
+
click.echo(
|
|
233
|
+
"Error: Missing PORTAINER_ACCESS_TOKEN environment variable", err=True
|
|
234
|
+
)
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
portainer_url = url.rstrip("/")
|
|
238
|
+
|
|
239
|
+
configs, status_code = list_configs(portainer_url, access_token, endpoint_id)
|
|
240
|
+
|
|
241
|
+
if status_code != 200:
|
|
242
|
+
click.echo(f"Error: Failed to list configs (HTTP {status_code})", err=True)
|
|
243
|
+
if configs:
|
|
244
|
+
click.echo(json.dumps(configs, indent=2), err=True)
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
|
|
247
|
+
if not configs:
|
|
248
|
+
click.echo("No configs found.")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
click.echo(f"Found {len(configs)} config(s):\n")
|
|
252
|
+
for config in configs:
|
|
253
|
+
spec = config.get("Spec", {})
|
|
254
|
+
config_id = config.get("ID", "unknown")
|
|
255
|
+
config_name = spec.get("Name", "unknown")
|
|
256
|
+
created = config.get("CreatedAt", "unknown")
|
|
257
|
+
click.echo(f" {config_name}")
|
|
258
|
+
click.echo(f" ID: {config_id}")
|
|
259
|
+
click.echo(f" Created: {created}")
|
|
260
|
+
click.echo()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@cli.command("delete")
|
|
264
|
+
@click.option("--url", "-u", required=True, help="Portainer base URL")
|
|
265
|
+
@click.option("--name", "-n", required=True, help="Config name")
|
|
266
|
+
@click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
|
|
267
|
+
def delete_config_cmd(url: str, name: str, endpoint_id: int):
|
|
268
|
+
"""Delete a Docker Swarm config by name."""
|
|
269
|
+
access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
|
|
270
|
+
if not access_token:
|
|
271
|
+
click.echo(
|
|
272
|
+
"Error: Missing PORTAINER_ACCESS_TOKEN environment variable", err=True
|
|
273
|
+
)
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
|
|
276
|
+
portainer_url = url.rstrip("/")
|
|
277
|
+
|
|
278
|
+
config = find_config_by_name(portainer_url, access_token, endpoint_id, name)
|
|
279
|
+
|
|
280
|
+
if not config:
|
|
281
|
+
click.echo(f"Error: Config '{name}' not found", err=True)
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
config_id = config.get("ID")
|
|
285
|
+
click.echo(f"Deleting config: {name} (ID: {config_id})")
|
|
286
|
+
|
|
287
|
+
_, status_code = delete_config(portainer_url, access_token, endpoint_id, config_id)
|
|
288
|
+
|
|
289
|
+
if status_code in (200, 204):
|
|
290
|
+
click.echo(f"✓ Config '{name}' deleted successfully")
|
|
291
|
+
else:
|
|
292
|
+
click.echo(f"Error: Failed to delete config (HTTP {status_code})", err=True)
|
|
293
|
+
sys.exit(1)
|