bow-cli 0.3.1__tar.gz → 0.4.4__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.
Files changed (90) hide show
  1. {bow_cli-0.3.1 → bow_cli-0.4.4}/PKG-INFO +2 -1
  2. {bow_cli-0.3.1 → bow_cli-0.4.4}/pyproject.toml +2 -1
  3. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/base.py +20 -27
  4. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/dependency.py +18 -10
  5. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/values.py +73 -4
  6. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/pull_cmd.py +1 -16
  7. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/push_cmd.py +4 -11
  8. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/registry_cmd.py +42 -1
  9. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/resource.py +2 -0
  10. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/resources.py +79 -5
  11. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/oci/__init__.py +2 -0
  12. bow_cli-0.4.4/src/bow/oci/client.py +658 -0
  13. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/oci/config.py +12 -2
  14. bow_cli-0.4.4/tests/postgresql/__init__.py +77 -0
  15. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_chart.py +1 -1
  16. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_charts.py +5 -40
  17. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_cli.py +1 -1
  18. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_stack.py +1 -1
  19. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_workspace.py +1 -5
  20. bow_cli-0.3.1/charts/valkey/pyproject.toml +0 -18
  21. bow_cli-0.3.1/charts/valkey/src/valkey/__init__.py +0 -96
  22. bow_cli-0.3.1/charts/valkey/src/valkey/defaults.yaml +0 -15
  23. bow_cli-0.3.1/src/bow/oci/client.py +0 -350
  24. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.analyze.agent.md +0 -0
  25. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.checklist.agent.md +0 -0
  26. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.clarify.agent.md +0 -0
  27. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.constitution.agent.md +0 -0
  28. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.implement.agent.md +0 -0
  29. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.plan.agent.md +0 -0
  30. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.specify.agent.md +0 -0
  31. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.tasks.agent.md +0 -0
  32. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.taskstoissues.agent.md +0 -0
  33. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.analyze.prompt.md +0 -0
  34. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.checklist.prompt.md +0 -0
  35. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.clarify.prompt.md +0 -0
  36. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.constitution.prompt.md +0 -0
  37. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.implement.prompt.md +0 -0
  38. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.plan.prompt.md +0 -0
  39. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.specify.prompt.md +0 -0
  40. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.tasks.prompt.md +0 -0
  41. {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.taskstoissues.prompt.md +0 -0
  42. {bow_cli-0.3.1 → bow_cli-0.4.4}/.gitignore +0 -0
  43. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/memory/constitution.md +0 -0
  44. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  45. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/common.sh +0 -0
  46. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/create-new-feature.sh +0 -0
  47. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/setup-plan.sh +0 -0
  48. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/update-agent-context.sh +0 -0
  49. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/agent-file-template.md +0 -0
  50. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/checklist-template.md +0 -0
  51. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/constitution-template.md +0 -0
  52. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/plan-template.md +0 -0
  53. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/spec-template.md +0 -0
  54. {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/tasks-template.md +0 -0
  55. {bow_cli-0.3.1 → bow_cli-0.4.4}/.vscode/launch.json +0 -0
  56. {bow_cli-0.3.1 → bow_cli-0.4.4}/.vscode/settings.json +0 -0
  57. {bow_cli-0.3.1 → bow_cli-0.4.4}/AGENTS.md +0 -0
  58. {bow_cli-0.3.1 → bow_cli-0.4.4}/README.md +0 -0
  59. {bow_cli-0.3.1 → bow_cli-0.4.4}/install.sh +0 -0
  60. {bow_cli-0.3.1 → bow_cli-0.4.4}/poetry.lock +0 -0
  61. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/__init__.py +0 -0
  62. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/__init__.py +0 -0
  63. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/registry.py +0 -0
  64. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/__init__.py +0 -0
  65. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/env_cmd.py +0 -0
  66. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/inspect_cmd.py +0 -0
  67. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/install_cmd.py +0 -0
  68. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/list_cmd.py +0 -0
  69. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/lock_cmd.py +0 -0
  70. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/status_cmd.py +0 -0
  71. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/template.py +0 -0
  72. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/uninstall_cmd.py +0 -0
  73. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/up.py +0 -0
  74. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/__init__.py +0 -0
  75. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/manifest.py +0 -0
  76. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/stack.py +0 -0
  77. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/oci/env.py +0 -0
  78. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/__init__.py +0 -0
  79. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/engine.py +0 -0
  80. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/merger.py +0 -0
  81. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/parser.py +0 -0
  82. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/refs.py +0 -0
  83. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/utils.py +0 -0
  84. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/__init__.py +0 -0
  85. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/lock.py +0 -0
  86. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/resolver.py +0 -0
  87. {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/stage.py +0 -0
  88. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_core.py +0 -0
  89. {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_oci.py +0 -0
  90. {bow_cli-0.3.1 → bow_cli-0.4.4}/uninstall.sh +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bow-cli
3
- Version: 0.3.1
3
+ Version: 0.4.4
4
4
  Summary: Pythonic Kubernetes DSL — As powerful as Helm, as easy as Pulumi, as readable as Python
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
7
7
  Requires-Dist: click>=8.0
8
8
  Requires-Dist: jq-utils<0.2.0,>=0.1.1
9
+ Requires-Dist: oras>=0.2.30
9
10
  Requires-Dist: pyyaml>=6.0
10
11
  Requires-Dist: semver>=3.0
11
12
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bow-cli"
7
- version = "0.3.1"
7
+ version = "0.4.4"
8
8
  description = "Pythonic Kubernetes DSL — As powerful as Helm, as easy as Pulumi, as readable as Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "click>=8.0",
15
15
  "semver>=3.0",
16
16
  "jq-utils (>=0.1.1,<0.2.0)",
17
+ "oras>=0.2.30",
17
18
  ]
18
19
 
19
20
  [project.scripts]
@@ -8,7 +8,6 @@ Render logic is written in Python, default values are read from YAML.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import importlib.metadata
12
11
  from pathlib import Path
13
12
  from typing import Any, ClassVar
14
13
 
@@ -54,29 +53,12 @@ class Chart:
54
53
  description: ClassVar[str] = ""
55
54
  requires: ClassVar[list[ChartDep]] = []
56
55
 
57
- def __init_subclass__(cls, **kwargs: Any) -> None:
58
- """Auto-populate version from package metadata."""
59
- super().__init_subclass__(**kwargs)
60
- # Skip for Chart base class itself
61
- if cls.__name__ == "Chart":
62
- return
63
- # Try to read version from package metadata
64
- # Package name patterns: bow-<name>, bow_<name>, <name>
65
- if cls.name:
66
- for pkg_name in [f"bow-{cls.name}", f"bow_{cls.name}", cls.name]:
67
- try:
68
- cls.version = importlib.metadata.version(pkg_name)
69
- break
70
- except importlib.metadata.PackageNotFoundError:
71
- continue
72
-
73
56
  def default_values(self) -> dict[str, Any]:
74
- """Return default values.
57
+ """Load defaults from defaults.yaml next to the chart module.
75
58
 
76
- First looks for defaults.yaml next to the chart module,
77
- returns an empty dict if not found. Subclass may override.
59
+ defaults.yaml is required. If missing, raises FileNotFoundError
60
+ so chart developers are forced to define all defaults explicitly.
78
61
  """
79
- # Look for defaults.yaml next to the chart module
80
62
  module_file = getattr(self.__class__, "__module__", None)
81
63
  if module_file:
82
64
  import importlib
@@ -87,10 +69,18 @@ class Chart:
87
69
  with open(defaults_path) as f:
88
70
  data = yaml.safe_load(f)
89
71
  return data if isinstance(data, dict) else {}
72
+ raise FileNotFoundError(
73
+ f"defaults.yaml not found for chart '{self.name}'. "
74
+ f"Expected at: {defaults_path}"
75
+ )
90
76
  return {}
91
77
 
92
- def render(self, values: dict[str, Any]) -> None:
93
- """Create resources. Subclass MUST implement."""
78
+ def render(self, values) -> None:
79
+ """Render chart resources. Subclass MUST implement.
80
+
81
+ Args:
82
+ values: A Values object with dot-access (v.service.port).
83
+ """
94
84
  raise NotImplementedError(f"{self.__class__.__name__}.render()")
95
85
 
96
86
  def template(
@@ -146,11 +136,14 @@ class Chart:
146
136
  f"Install it with: pip install bow-{dep.chart}"
147
137
  )
148
138
 
149
- # Dependency values
150
- dep_values = get_dep_values(values, dep)
139
+ # Dependency values: dep chart defaults → dep.default_values → parent overrides
140
+ dep_defaults = dep_chart.default_values()
141
+ dep_overrides = get_dep_values(values, dep)
142
+ from bow.chart.values import deep_merge as _dm, Values
143
+ dep_values = _dm(dep_defaults, dep_overrides)
151
144
 
152
- # Render
153
- dep_chart.render(dep_values)
145
+ # Render (wrap in Values for dot-access)
146
+ dep_chart.render(Values(dep_values))
154
147
 
155
148
  def info(self) -> dict[str, Any]:
156
149
  """Return chart information (for bow inspect)."""
@@ -28,8 +28,10 @@ class ChartDep:
28
28
  default_values: dict[str, Any] = field(default_factory=dict)
29
29
 
30
30
 
31
- def resolve_condition(values: dict, condition: str | None) -> bool:
32
- """Check the condition string in the values dict.
31
+ def resolve_condition(values, condition: str | None) -> bool:
32
+ """Check a dot-path condition against values.
33
+
34
+ Works with both dict and Values objects.
33
35
 
34
36
  >>> resolve_condition({"postgresql": {"enabled": True}}, "postgresql.enabled")
35
37
  True
@@ -44,22 +46,28 @@ def resolve_condition(values: dict, condition: str | None) -> bool:
44
46
  parts = condition.split(".")
45
47
  current: Any = values
46
48
  for part in parts:
47
- if isinstance(current, dict) and part in current:
49
+ if _is_mapping(current) and part in current:
48
50
  current = current[part]
49
51
  else:
50
- return True # Key not found, default enabled
52
+ return True # Key missing = default enabled
51
53
  return bool(current)
52
54
 
53
55
 
54
- def get_dep_values(values: dict, dep: ChartDep) -> dict:
55
- """Extract values for the dependency chart.
56
+ def get_dep_values(values, dep: ChartDep) -> dict:
57
+ """Extract dependency values from parent chart values.
56
58
 
57
- Uses the nested dict matching the dependency name in the
58
- main chart's values; falls back to default_values.
59
+ Works with both dict and Values objects.
60
+ Returns a plain dict (dependency chart wraps it in Values itself).
59
61
  """
60
62
  from bow.chart.values import deep_merge
61
63
 
62
64
  result = dict(dep.default_values)
63
- if dep.chart in values and isinstance(values[dep.chart], dict):
64
- result = deep_merge(result, values[dep.chart])
65
+ raw = values._data if hasattr(values, "_data") else values
66
+ if dep.chart in raw and isinstance(raw[dep.chart], dict):
67
+ result = deep_merge(result, raw[dep.chart])
65
68
  return result
69
+
70
+
71
+ def _is_mapping(obj) -> bool:
72
+ """Check if obj supports dict-like access (dict or Values)."""
73
+ return isinstance(obj, dict) or hasattr(obj, "_data")
@@ -68,7 +68,7 @@ def parse_set_values(set_args: list[str]) -> dict:
68
68
 
69
69
 
70
70
  def _coerce_value(value: str) -> Any:
71
- """Convert a string value to the appropriate Python type.
71
+ """String değeri uygun Python tipine çevir.
72
72
 
73
73
  >>> _coerce_value("3")
74
74
  3
@@ -98,8 +98,8 @@ def merge_all_values(
98
98
  defaults: dict,
99
99
  value_files: list[str | Path],
100
100
  set_args: list[str],
101
- ) -> dict:
102
- """Merge all value sources.
101
+ ) -> Values:
102
+ """Merge all value sources and return a Values object.
103
103
 
104
104
  Precedence (low to high):
105
105
  defaults → value_files (in order) → set_args
@@ -111,4 +111,73 @@ def merge_all_values(
111
111
  if set_args:
112
112
  set_values = parse_set_values(set_args)
113
113
  result = deep_merge(result, set_values)
114
- return result
114
+ return Values(result)
115
+
116
+
117
+ class Values:
118
+ """Dot-notation access wrapper over a dict.
119
+
120
+ Provides clean attribute-style access to nested config::
121
+
122
+ v.service.port # instead of values["service"]["port"]
123
+ v.resources.cpu # nested access
124
+ v.replicas # scalar
125
+
126
+ If a key is missing, raises AttributeError with a clear message
127
+ pointing to the missing key path (means defaults.yaml is incomplete).
128
+
129
+ The underlying dict is always accessible via ``v._data``.
130
+ Dict-style access (v["key"]) and iteration also work.
131
+ """
132
+
133
+ __slots__ = ("_data",)
134
+
135
+ def __init__(self, data: dict[str, Any]):
136
+ object.__setattr__(self, "_data", data)
137
+
138
+ def __getattr__(self, key: str) -> Any:
139
+ try:
140
+ val = self._data[key]
141
+ except KeyError:
142
+ raise AttributeError(
143
+ f"Value '{key}' not found. "
144
+ f"Add it to defaults.yaml or pass via -f / --set"
145
+ ) from None
146
+ if isinstance(val, dict):
147
+ return Values(val)
148
+ return val
149
+
150
+ def __getitem__(self, key: str) -> Any:
151
+ val = self._data[key]
152
+ if isinstance(val, dict):
153
+ return Values(val)
154
+ return val
155
+
156
+ def __contains__(self, key: str) -> bool:
157
+ return key in self._data
158
+
159
+ def __iter__(self):
160
+ return iter(self._data)
161
+
162
+ def __repr__(self) -> str:
163
+ return f"Values({self._data!r})"
164
+
165
+ def __bool__(self) -> bool:
166
+ return bool(self._data)
167
+
168
+ def get(self, key: str, default: Any = None) -> Any:
169
+ """Dict-compatible get with fallback."""
170
+ val = self._data.get(key, default)
171
+ if isinstance(val, dict):
172
+ return Values(val)
173
+ return val
174
+
175
+ def items(self):
176
+ return self._data.items()
177
+
178
+ def keys(self):
179
+ return self._data.keys()
180
+
181
+ def to_dict(self) -> dict[str, Any]:
182
+ """Return the underlying raw dict."""
183
+ return self._data
@@ -22,15 +22,6 @@ def pull_cmd(reference):
22
22
  # Parse reference: name:version or oci://reg/name:version
23
23
  name, version, registry_url = _parse_reference(reference, cfg)
24
24
 
25
- # Whitelist check
26
- if not cfg.is_registry_allowed(registry_url):
27
- click.echo(
28
- f"❌ Registry not allowed: {registry_url}\n"
29
- f"Add it to security.allowed_registries in ~/.bow/config.yaml",
30
- err=True,
31
- )
32
- sys.exit(1)
33
-
34
25
  # Pull
35
26
  try:
36
27
  env = get_env()
@@ -83,13 +74,7 @@ def _parse_reference(ref: str, cfg) -> tuple[str, str, str]:
83
74
  else:
84
75
  # postgresql:16.4.0 — use default registry
85
76
  name_version = ref
86
- default_reg = cfg.default_registry()
87
- if default_reg is None:
88
- raise click.BadParameter(
89
- "No default registry configured. "
90
- "Run: bow registry add default oci://..."
91
- )
92
- registry_base = default_reg.url
77
+ registry_base = cfg.default_registry().url
93
78
 
94
79
  if ":" in name_version:
95
80
  name, version = name_version.rsplit(":", 1)
@@ -3,7 +3,7 @@ bow.cli.push_cmd — bow push command.
3
3
 
4
4
  bow push . --tag 16.4.0
5
5
  bow push ./charts/bow-postgresql
6
- bow push . --registry oci://harbor.internal/charts
6
+ bow push . --registry oci://ghcr.io/getbow/charts
7
7
  """
8
8
 
9
9
  import sys
@@ -21,17 +21,9 @@ def push_cmd(chart_dir, tag, registry):
21
21
 
22
22
  cfg = load_config()
23
23
 
24
- # Resolve registry
24
+ # Resolve registry (always has a default now)
25
25
  if registry is None:
26
- default_reg = cfg.default_registry()
27
- if default_reg is None:
28
- click.echo(
29
- "Error: No registry specified and no default configured.\n"
30
- "Run: bow registry add default oci://...",
31
- err=True,
32
- )
33
- sys.exit(1)
34
- registry = default_reg.url
26
+ registry = cfg.default_registry().url
35
27
 
36
28
  # Package
37
29
  try:
@@ -48,6 +40,7 @@ def push_cmd(chart_dir, tag, registry):
48
40
  )
49
41
 
50
42
  # Push
43
+ click.echo(f"Pushing to {registry}...", err=True)
51
44
  ref = push_chart(artifact, registry)
52
45
  click.echo(f"✓ Pushed to {ref}", err=True)
53
46
 
@@ -1,10 +1,12 @@
1
1
  """
2
2
  bow.cli.registry_cmd — bow registry command.
3
3
 
4
- bow registry add default oci://ghcr.io/myorg/charts
4
+ bow registry add default oci://ghcr.io/getbow/charts
5
5
  bow registry add harbor oci://harbor.internal/bow --default
6
6
  bow registry list
7
7
  bow registry remove harbor
8
+ bow registry login ghcr.io
9
+ bow registry login ghcr.io -u USERNAME -p TOKEN
8
10
  """
9
11
 
10
12
  import sys
@@ -17,6 +19,45 @@ def registry_cmd():
17
19
  pass
18
20
 
19
21
 
22
+ @registry_cmd.command("login")
23
+ @click.argument("hostname")
24
+ @click.option("--username", "-u", default=None, help="Registry username")
25
+ @click.option("--password", "-p", default=None,
26
+ help="Registry password/token (or use --password-stdin)")
27
+ @click.option("--password-stdin", is_flag=True,
28
+ help="Read password from stdin")
29
+ def registry_login(hostname, username, password, password_stdin):
30
+ """Login to an OCI registry.
31
+
32
+ \b
33
+ Examples:
34
+ bow registry login ghcr.io
35
+ bow registry login ghcr.io -u USERNAME -p ghp_TOKEN
36
+ echo $GITHUB_TOKEN | bow registry login ghcr.io -u USERNAME --password-stdin
37
+ """
38
+ import oras.client
39
+
40
+ if password_stdin:
41
+ password = sys.stdin.readline().strip()
42
+
43
+ if not username:
44
+ username = click.prompt("Username")
45
+ if not password:
46
+ password = click.prompt("Password/Token", hide_input=True)
47
+
48
+ try:
49
+ client = oras.client.OrasClient()
50
+ client.login(
51
+ hostname=hostname,
52
+ username=username,
53
+ password=password,
54
+ )
55
+ click.echo(f"✓ Logged in to {hostname}")
56
+ except Exception as e:
57
+ click.echo(f"Error: Login failed: {e}", err=True)
58
+ sys.exit(1)
59
+
60
+
20
61
  @registry_cmd.command("add")
21
62
  @click.argument("name")
22
63
  @click.argument("url")
@@ -70,6 +70,8 @@ class Resource:
70
70
  self.props = kwargs
71
71
 
72
72
  # Attach to parent or register as top-level
73
+ if self.enabled is False:
74
+ return
73
75
  parent = _current()
74
76
  if parent is not None:
75
77
  parent._adopt(self)
@@ -348,6 +348,8 @@ class Deployment(Resource):
348
348
  child.selector = dict(self.labels)
349
349
  self.services.append(child)
350
350
  elif isinstance(child, PersistentVolumeClaim):
351
+ if child.enabled is False:
352
+ return
351
353
  self.pvcs.append(child)
352
354
  self.volumes.append({
353
355
  "name": child.claim_name,
@@ -423,6 +425,8 @@ class StatefulSet(Resource):
423
425
  child.selector = dict(self.labels)
424
426
  self.services.append(child)
425
427
  elif isinstance(child, PersistentVolumeClaim):
428
+ if child.enabled is False:
429
+ return
426
430
  self.volume_claim_templates.append(child.render_template())
427
431
  else:
428
432
  super()._adopt(child)
@@ -464,6 +468,9 @@ class StatefulSet(Resource):
464
468
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
465
469
  # SERVICE — both leaf and with
466
470
  # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
471
+ _UNSET = object() # Sentinel value for cluster_ip
472
+
473
+
467
474
  class ServicePort:
468
475
  """Service port definition. Used as a leaf inside a Service context."""
469
476
 
@@ -519,6 +526,7 @@ class Service(Resource):
519
526
  ):
520
527
  self.type = type
521
528
  self.selector: dict = kwargs.pop("selector", {})
529
+ self.cluster_ip = kwargs.pop("cluster_ip", _UNSET)
522
530
  self.ports: list[dict] = []
523
531
 
524
532
  # Leaf mode: if port is provided, add it immediately
@@ -538,15 +546,23 @@ class Service(Resource):
538
546
  pass # ServicePort adds directly to self.ports
539
547
 
540
548
  def render(self) -> dict[str, Any]:
549
+ spec: dict[str, Any] = {
550
+ "type": self.type,
551
+ "selector": self.selector,
552
+ "ports": self.ports,
553
+ }
554
+ if self.cluster_ip is not _UNSET:
555
+ # User explicitly set cluster_ip
556
+ if self.cluster_ip is None:
557
+ # For headless services: cluster_ip=None → clusterIP: "None"
558
+ spec["clusterIP"] = "None"
559
+ else:
560
+ spec["clusterIP"] = self.cluster_ip
541
561
  return {
542
562
  "apiVersion": self._api_version,
543
563
  "kind": self._kind,
544
564
  "metadata": {"name": self.metadata["name"]},
545
- "spec": {
546
- "type": self.type,
547
- "selector": self.selector,
548
- "ports": self.ports,
549
- },
565
+ "spec": spec,
550
566
  }
551
567
 
552
568
 
@@ -762,3 +778,61 @@ class CronJob(Resource):
762
778
  },
763
779
  },
764
780
  }
781
+
782
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
783
+ # ENV HELPERS
784
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
785
+ def env_from(credentials, *keys: str) -> None:
786
+ """Set env vars from a credentials block.
787
+
788
+ Routes each key to the right source based on what's
789
+ configured in the credentials block:
790
+
791
+ defaults.yaml — plain values (default)::
792
+
793
+ credentials:
794
+ POSTGRES_USER: postgres
795
+ POSTGRES_DB: appdb
796
+ POSTGRES_PASSWORD: secret123
797
+
798
+ user overrides with secret::
799
+
800
+ credentials:
801
+ secret_ref: pg-credentials
802
+
803
+ user overrides with configmap::
804
+
805
+ credentials:
806
+ configmap_ref: pg-config
807
+
808
+ mixed — configmap with one value override::
809
+
810
+ credentials:
811
+ configmap_ref: pg-config
812
+ POSTGRES_DB: override_db
813
+
814
+ Chart code (always the same regardless of source)::
815
+
816
+ env_from(v.credentials,
817
+ "POSTGRES_USER", "POSTGRES_DB", "POSTGRES_PASSWORD")
818
+ """
819
+ data = credentials._data if hasattr(credentials, "_data") else credentials
820
+ if not isinstance(data, dict):
821
+ data = {}
822
+
823
+ secret_ref = data.get("secret_ref")
824
+ secrets = data.get("secrets")
825
+ configmap_ref = data.get("configmap_ref")
826
+ configmaps = data.get("configmaps")
827
+ reserved = {"secret_ref", "configmap_ref"}
828
+
829
+ for key in keys:
830
+ if key in data and key not in reserved:
831
+ # Explicit value — always wins
832
+ EnvVar(key, value=data[key])
833
+ elif key in secrets and secret_ref:
834
+ EnvVar(key, secret_ref=secret_ref, secret_key=secrets[key])
835
+ elif key in configmaps and configmap_ref:
836
+ EnvVar(key, configmap_ref=configmap_ref, configmap_key=configmaps[key])
837
+ else:
838
+ EnvVar(key, value="")
@@ -2,6 +2,7 @@
2
2
 
3
3
  from bow.oci.config import (
4
4
  BowConfig, RegistryConfig, load_config, save_config, BOW_HOME,
5
+ DEFAULT_REGISTRY_URL, DEFAULT_REGISTRY_NAME,
5
6
  )
6
7
  from bow.oci.env import (
7
8
  EnvInfo, EnvError,
@@ -15,6 +16,7 @@ from bow.oci.client import (
15
16
 
16
17
  __all__ = [
17
18
  "BowConfig", "RegistryConfig", "load_config", "save_config", "BOW_HOME",
19
+ "DEFAULT_REGISTRY_URL", "DEFAULT_REGISTRY_NAME",
18
20
  "EnvInfo", "EnvError",
19
21
  "create_env", "delete_env", "get_env", "use_env", "list_envs",
20
22
  "resolve_active_env", "pip_install_in_env",