docker-stack 2.0.2__py3-none-any.whl → 2.0.4__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.
docker_stack/cli.py CHANGED
@@ -303,23 +303,59 @@ class DockerStack:
303
303
  Returns a tuple: (processed_objects, commands)
304
304
  """
305
305
  processed_objects = {}
306
+ manager_client = self._manager_client_for_feature(FEATURE_STACK_DEPLOY)
307
+ namespace = os.getenv("DOCKER_STACK_NAMESPACE", "default")
306
308
 
307
- def add_obj(name, data, explicit_name=None, is_generated_secret=False):
309
+ def docker_object_name_for(name, explicit_name=None):
310
+ if explicit_name:
311
+ return explicit_name
312
+ if stack:
313
+ return f"{stack}_{name}"
314
+ return name
315
+
316
+ def normalize_labels(labels):
317
+ normalized = {}
318
+ for raw in labels:
319
+ key, _, value = raw.partition("=")
320
+ if key and _:
321
+ normalized[key] = value
322
+ return normalized
323
+
324
+ def add_obj(name, data, explicit_name=None, is_generated_secret=False, generate_options=None):
308
325
  labels = []
309
326
  if is_generated_secret:
310
327
  labels.append("mesudip.secret.generated=true")
311
328
 
312
- if explicit_name:
313
- docker_object_name = explicit_name
314
- elif stack:
315
- docker_object_name = f"{stack}_{name}"
316
- else:
317
- docker_object_name = name
329
+ docker_object_name = docker_object_name_for(name, explicit_name=explicit_name)
330
+
331
+ if manager_client and stack:
332
+ label_map = normalize_labels(labels)
333
+ if manager.object_type == "config":
334
+ response = manager_client.resolve_config(
335
+ stack=stack,
336
+ namespace=namespace,
337
+ name=docker_object_name,
338
+ content=data,
339
+ labels=label_map,
340
+ )
341
+ else:
342
+ response = manager_client.resolve_secret(
343
+ stack=stack,
344
+ namespace=namespace,
345
+ name=docker_object_name,
346
+ content=None if is_generated_secret else data,
347
+ generate=generate_options if is_generated_secret else None,
348
+ labels=label_map,
349
+ return_generated_value=is_generated_secret,
350
+ )
351
+ if is_generated_secret and response.get("generated_value"):
352
+ self.generated_secrets[name] = response["generated_value"]
353
+ processed_objects[name] = {"name": response["actual_name"], "external": True}
354
+ return
318
355
 
319
356
  (object_name, command) = manager.create(docker_object_name, data, labels=labels, stack=stack)
320
357
  if not command.isNop():
321
358
  self.commands.append(command)
322
- # If a new secret was actually created (not just reused), store it
323
359
  if is_generated_secret:
324
360
  self.generated_secrets[name] = data
325
361
  processed_objects[name] = {"name": object_name, "external": True}
@@ -338,28 +374,40 @@ class DockerStack:
338
374
  with open(filename) as file:
339
375
  add_obj(name, file.read(), explicit_name=explicit_name)
340
376
  elif isinstance(details, dict) and "x-generate" in details and manager.object_type == "secret":
341
- is_generated_secret = True
342
377
  generate_options = details["x-generate"]
343
- secret_content = ""
344
-
345
- # Determine the content to be used for the secret
346
- # This logic is now simplified as DockerObjectManager.create handles persistence
347
378
  if isinstance(generate_options, bool) and generate_options:
348
- secret_content = generate_secret()
379
+ if manager_client and stack:
380
+ add_obj(name, "", explicit_name=explicit_name, is_generated_secret=True, generate_options={})
381
+ else:
382
+ add_obj(name, generate_secret(), explicit_name=explicit_name, is_generated_secret=True)
349
383
  elif isinstance(generate_options, int):
350
- secret_content = generate_secret(length=generate_options)
384
+ if manager_client and stack:
385
+ add_obj(name, "", explicit_name=explicit_name, is_generated_secret=True, generate_options={"length": generate_options})
386
+ else:
387
+ add_obj(name, generate_secret(length=generate_options), explicit_name=explicit_name, is_generated_secret=True)
351
388
  elif isinstance(generate_options, dict):
352
- secret_content = generate_secret(
353
- length=generate_options.get("length"),
354
- numbers=generate_options.get("numbers", True),
355
- special=generate_options.get("special", True),
356
- uppercase=generate_options.get("uppercase", True),
357
- )
389
+ options = {
390
+ "length": generate_options.get("length"),
391
+ "numbers": generate_options.get("numbers", True),
392
+ "special": generate_options.get("special", True),
393
+ "uppercase": generate_options.get("uppercase", True),
394
+ }
395
+ if manager_client and stack:
396
+ add_obj(name, "", explicit_name=explicit_name, is_generated_secret=True, generate_options=options)
397
+ else:
398
+ add_obj(
399
+ name,
400
+ generate_secret(
401
+ length=options.get("length"),
402
+ numbers=options.get("numbers", True),
403
+ special=options.get("special", True),
404
+ uppercase=options.get("uppercase", True),
405
+ ),
406
+ explicit_name=explicit_name,
407
+ is_generated_secret=True,
408
+ )
358
409
  else:
359
410
  raise ValueError(f"Invalid x-generate value for secret {name}: {generate_options}")
360
-
361
- # Call add_obj with the potentially new secret content and the generated flag
362
- add_obj(name, secret_content, explicit_name=explicit_name, is_generated_secret=True)
363
411
  else:
364
412
  processed_objects[name] = details
365
413
  return processed_objects
@@ -110,7 +110,16 @@ class ManagerApiClient:
110
110
  with urllib.request.urlopen(request, timeout=self.timeout_secs, context=context) as response:
111
111
  return json.loads(response.read().decode("utf-8"))
112
112
  except urllib.error.HTTPError as exc:
113
- raise RuntimeError(f"Manager request failed ({method} {path}): HTTP {exc.code}") from exc
113
+ body = ""
114
+ try:
115
+ payload = exc.read().decode("utf-8", errors="replace").strip()
116
+ if payload:
117
+ body = f": {payload}"
118
+ except Exception:
119
+ body = ""
120
+ raise RuntimeError(
121
+ f"Manager request failed ({method} {path}): HTTP {exc.code}{body}"
122
+ ) from exc
114
123
  except urllib.error.URLError as exc:
115
124
  raise RuntimeError(f"Manager request failed ({method} {path}): {exc.reason}") from exc
116
125
  except json.JSONDecodeError as exc:
@@ -249,6 +258,60 @@ class ManagerApiClient:
249
258
 
250
259
  return {"nodes": nodes}
251
260
 
261
+
262
+
263
+ def resolve_config(
264
+ self,
265
+ *,
266
+ stack: str,
267
+ namespace: str,
268
+ name: str,
269
+ content: str,
270
+ labels: Optional[Dict[str, str]] = None,
271
+ ) -> Dict[str, Any]:
272
+ payload: Dict[str, Any] = {
273
+ "stack": stack,
274
+ "namespace": namespace,
275
+ "name": name,
276
+ "content": content,
277
+ }
278
+ if labels:
279
+ payload["labels"] = labels
280
+ return self._request_json(
281
+ "/api/configs/resolve",
282
+ method="POST",
283
+ payload=payload,
284
+ )
285
+
286
+ def resolve_secret(
287
+ self,
288
+ *,
289
+ stack: str,
290
+ namespace: str,
291
+ name: str,
292
+ content: Optional[str] = None,
293
+ generate: Optional[Dict[str, Any]] = None,
294
+ labels: Optional[Dict[str, str]] = None,
295
+ return_generated_value: bool = False,
296
+ ) -> Dict[str, Any]:
297
+ payload: Dict[str, Any] = {
298
+ "stack": stack,
299
+ "namespace": namespace,
300
+ "name": name,
301
+ "return_generated_value": return_generated_value,
302
+ }
303
+ if content is not None:
304
+ payload["content"] = content
305
+ if generate is not None:
306
+ payload["generate"] = generate
307
+ if labels:
308
+ payload["labels"] = labels
309
+ return self._request_json(
310
+ "/api/secrets/resolve",
311
+ method="POST",
312
+ payload=payload,
313
+ )
314
+
252
315
  def validate_stack(
253
316
  self,
254
317
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-stack
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: CLI for deploying and managing Docker stacks.
5
5
  Home-page: https://github.com/mesudip/docker-stack
6
6
  Author: Sudip Bhattarai
@@ -1,5 +1,5 @@
1
1
  docker_stack/__init__.py,sha256=22W2926fX4sMA1POE1EligVb05cbmP8I6rnacEUmaFs,879
2
- docker_stack/cli.py,sha256=fsjXjaGFBXi-uYtAm5jcYSPg-yafaVsENsjKK9o_55g,50570
2
+ docker_stack/cli.py,sha256=UJ7ux33-EAZtqY3lwMyFlg95PJWsAnarBRkjMKU3RyM,53053
3
3
  docker_stack/command_runner.py,sha256=mNaUAVKtrJbji_5ARVPLKhc92avqllNKrVQj1A6S0dA,1252
4
4
  docker_stack/compose.py,sha256=_fAVesjyW5ecid46sYYhcu02yF1vqknDSCFYflJPDOE,534
5
5
  docker_stack/docker_objects.py,sha256=6FYi3Cq5qTkz2XgRrb_DGy2tgX8uoagib6bZD_Bs3W4,10922
@@ -7,13 +7,13 @@ docker_stack/envsubst.py,sha256=gdEUpsjWJeNbbahySk56vUdQh1nns85o3LbU6SUBo3I,8337
7
7
  docker_stack/envsubst_merge.py,sha256=TwOu8BducG0rWnSreJGwesNi-VghCetljcf-aWceE8M,4971
8
8
  docker_stack/helpers.py,sha256=7zQT_lYU6IcqjSOYjE0JlHRtbB8Sffz3XlJKyNvot6U,5580
9
9
  docker_stack/login.py,sha256=_4sx0hltvKnwA81r_HyYkdQjXs5AEPlh_Qy87sUfcP0,30996
10
- docker_stack/manager_api.py,sha256=MrPKfbykRHgGoEl-1qehJBdaxncXGvzN_ef5Q2QBPXk,11486
10
+ docker_stack/manager_api.py,sha256=NsSuXV7b5CiBDOz5EqqDWE4xwzaXNoqldrsd2oKAziw,13205
11
11
  docker_stack/markers.py,sha256=gqStnr0tNWZ1kzqvREc2jVQVQrevheShgJtL5QzSx4g,2388
12
12
  docker_stack/merge_conf.py,sha256=Pmsabcgf3SDyaWvf2OLsBPodzEqsvOIjj9xzn_qWXH0,1917
13
13
  docker_stack/registry.py,sha256=sWC1J9JDIrcYyhei1POYCZHJ0xUCKC2pveisSHMgYsQ,9036
14
14
  docker_stack/url_parser.py,sha256=Sk8GQE0nEiwCkijp1OltP2AfgtKEmM2hybcH_rBhfiI,6824
15
- docker_stack-2.0.2.dist-info/METADATA,sha256=GUpDIRMWkpz6IFjZUGhHU9qhaEMmU2WYfyEweLaLyVE,8753
16
- docker_stack-2.0.2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
17
- docker_stack-2.0.2.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
18
- docker_stack-2.0.2.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
19
- docker_stack-2.0.2.dist-info/RECORD,,
15
+ docker_stack-2.0.4.dist-info/METADATA,sha256=FIag9JDv5WzZ6yOH1AurLdAnKHOcHZO-YFSl_r6YN5g,8753
16
+ docker_stack-2.0.4.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
17
+ docker_stack-2.0.4.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
18
+ docker_stack-2.0.4.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
19
+ docker_stack-2.0.4.dist-info/RECORD,,