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.
- {bow_cli-0.3.1 → bow_cli-0.4.4}/PKG-INFO +2 -1
- {bow_cli-0.3.1 → bow_cli-0.4.4}/pyproject.toml +2 -1
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/base.py +20 -27
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/dependency.py +18 -10
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/values.py +73 -4
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/pull_cmd.py +1 -16
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/push_cmd.py +4 -11
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/registry_cmd.py +42 -1
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/resource.py +2 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/resources.py +79 -5
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/oci/__init__.py +2 -0
- bow_cli-0.4.4/src/bow/oci/client.py +658 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/oci/config.py +12 -2
- bow_cli-0.4.4/tests/postgresql/__init__.py +77 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_chart.py +1 -1
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_charts.py +5 -40
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_cli.py +1 -1
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_stack.py +1 -1
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_workspace.py +1 -5
- bow_cli-0.3.1/charts/valkey/pyproject.toml +0 -18
- bow_cli-0.3.1/charts/valkey/src/valkey/__init__.py +0 -96
- bow_cli-0.3.1/charts/valkey/src/valkey/defaults.yaml +0 -15
- bow_cli-0.3.1/src/bow/oci/client.py +0 -350
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.analyze.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.checklist.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.clarify.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.constitution.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.implement.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.plan.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.specify.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.tasks.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/agents/speckit.taskstoissues.agent.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.analyze.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.checklist.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.clarify.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.constitution.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.implement.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.plan.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.specify.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.tasks.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.github/prompts/speckit.taskstoissues.prompt.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.gitignore +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/memory/constitution.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/common.sh +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/setup-plan.sh +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/scripts/bash/update-agent-context.sh +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/agent-file-template.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/checklist-template.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/constitution-template.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/plan-template.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/spec-template.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.specify/templates/tasks-template.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.vscode/launch.json +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/.vscode/settings.json +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/AGENTS.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/README.md +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/install.sh +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/poetry.lock +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/__init__.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/__init__.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/chart/registry.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/__init__.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/env_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/inspect_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/install_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/list_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/lock_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/status_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/template.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/uninstall_cmd.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/cli/up.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/__init__.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/manifest.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/core/stack.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/oci/env.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/__init__.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/engine.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/merger.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/parser.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/stack/refs.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/utils.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/__init__.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/lock.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/resolver.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/src/bow/workspace/stage.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_core.py +0 -0
- {bow_cli-0.3.1 → bow_cli-0.4.4}/tests/test_oci.py +0 -0
- {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
|
+
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.
|
|
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
|
-
"""
|
|
57
|
+
"""Load defaults from defaults.yaml next to the chart module.
|
|
75
58
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
93
|
-
"""
|
|
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
|
-
|
|
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
|
|
32
|
-
"""Check
|
|
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
|
|
49
|
+
if _is_mapping(current) and part in current:
|
|
48
50
|
current = current[part]
|
|
49
51
|
else:
|
|
50
|
-
return True # Key
|
|
52
|
+
return True # Key missing = default enabled
|
|
51
53
|
return bool(current)
|
|
52
54
|
|
|
53
55
|
|
|
54
|
-
def get_dep_values(values
|
|
55
|
-
"""Extract values
|
|
56
|
+
def get_dep_values(values, dep: ChartDep) -> dict:
|
|
57
|
+
"""Extract dependency values from parent chart values.
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
"""
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
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/
|
|
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")
|
|
@@ -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",
|