bow-cli 0.4.2__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.4.2 → bow_cli-0.4.4}/PKG-INFO +1 -1
- {bow_cli-0.4.2 → bow_cli-0.4.4}/pyproject.toml +1 -1
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/base.py +20 -27
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/dependency.py +18 -10
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/values.py +73 -4
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/resources.py +75 -5
- bow_cli-0.4.4/tests/postgresql/__init__.py +77 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_chart.py +1 -1
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_charts.py +5 -40
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_cli.py +1 -1
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_stack.py +1 -1
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_workspace.py +1 -5
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.analyze.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.checklist.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.clarify.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.constitution.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.implement.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.plan.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.specify.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.tasks.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.taskstoissues.agent.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.analyze.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.checklist.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.clarify.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.constitution.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.implement.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.plan.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.specify.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.tasks.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.taskstoissues.prompt.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.gitignore +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/memory/constitution.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/common.sh +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/setup-plan.sh +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/update-agent-context.sh +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/agent-file-template.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/checklist-template.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/constitution-template.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/plan-template.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/spec-template.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/tasks-template.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.vscode/launch.json +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/.vscode/settings.json +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/AGENTS.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/README.md +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/install.sh +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/poetry.lock +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/registry.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/env_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/inspect_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/install_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/list_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/lock_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/pull_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/push_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/registry_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/status_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/template.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/uninstall_cmd.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/up.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/manifest.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/resource.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/stack.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/client.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/config.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/env.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/engine.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/merger.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/parser.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/refs.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/utils.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/__init__.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/lock.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/resolver.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/stage.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_core.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_oci.py +0 -0
- {bow_cli-0.4.2 → bow_cli-0.4.4}/uninstall.sh +0 -0
|
@@ -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
|
|
@@ -468,6 +468,9 @@ class StatefulSet(Resource):
|
|
|
468
468
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
469
469
|
# SERVICE — both leaf and with
|
|
470
470
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
471
|
+
_UNSET = object() # Sentinel value for cluster_ip
|
|
472
|
+
|
|
473
|
+
|
|
471
474
|
class ServicePort:
|
|
472
475
|
"""Service port definition. Used as a leaf inside a Service context."""
|
|
473
476
|
|
|
@@ -523,6 +526,7 @@ class Service(Resource):
|
|
|
523
526
|
):
|
|
524
527
|
self.type = type
|
|
525
528
|
self.selector: dict = kwargs.pop("selector", {})
|
|
529
|
+
self.cluster_ip = kwargs.pop("cluster_ip", _UNSET)
|
|
526
530
|
self.ports: list[dict] = []
|
|
527
531
|
|
|
528
532
|
# Leaf mode: if port is provided, add it immediately
|
|
@@ -542,15 +546,23 @@ class Service(Resource):
|
|
|
542
546
|
pass # ServicePort adds directly to self.ports
|
|
543
547
|
|
|
544
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
|
|
545
561
|
return {
|
|
546
562
|
"apiVersion": self._api_version,
|
|
547
563
|
"kind": self._kind,
|
|
548
564
|
"metadata": {"name": self.metadata["name"]},
|
|
549
|
-
"spec":
|
|
550
|
-
"type": self.type,
|
|
551
|
-
"selector": self.selector,
|
|
552
|
-
"ports": self.ports,
|
|
553
|
-
},
|
|
565
|
+
"spec": spec,
|
|
554
566
|
}
|
|
555
567
|
|
|
556
568
|
|
|
@@ -766,3 +778,61 @@ class CronJob(Resource):
|
|
|
766
778
|
},
|
|
767
779
|
},
|
|
768
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="")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bow.charts.postgresql — PostgreSQL chart.
|
|
3
|
+
|
|
4
|
+
Minimal PostgreSQL deployment for Kubernetes using bow DSL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from bow import Deployment
|
|
12
|
+
from bow.chart.base import Chart
|
|
13
|
+
from bow.core.resources import (
|
|
14
|
+
StatefulSet, Container, Secret, Service, PersistentVolumeClaim,
|
|
15
|
+
Port, EnvVar, Resources, Probe
|
|
16
|
+
)
|
|
17
|
+
from bow.utils import full_image, apply_resources
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
21
|
+
# REUSABLE COMPONENTS
|
|
22
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
23
|
+
@contextmanager
|
|
24
|
+
def postgresql_container(
|
|
25
|
+
name: str,
|
|
26
|
+
image: str,
|
|
27
|
+
port: int = 5432,
|
|
28
|
+
secret_name: str = "postgres",
|
|
29
|
+
postgres_user: str = "postgres",
|
|
30
|
+
postgres_db: str = "postgres",
|
|
31
|
+
resources: dict | None = None) -> None:
|
|
32
|
+
"""PostgreSQL main container component."""
|
|
33
|
+
with Container(name, image=image):
|
|
34
|
+
Port(port, name="postgresql")
|
|
35
|
+
|
|
36
|
+
# Core postgres settings
|
|
37
|
+
EnvVar("POSTGRES_USER", postgres_user)
|
|
38
|
+
EnvVar("POSTGRES_DB", postgres_db)
|
|
39
|
+
EnvVar("POSTGRES_PASSWORD", secret_ref=secret_name, secret_key="POSTGRES_PASSWORD")
|
|
40
|
+
EnvVar("PGDATA", "/var/lib/postgresql/data")
|
|
41
|
+
|
|
42
|
+
apply_resources(resources)
|
|
43
|
+
yield
|
|
44
|
+
|
|
45
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
46
|
+
# CHART CLASS
|
|
47
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
48
|
+
class PostgreSQLChart(Chart):
|
|
49
|
+
name = "postgresql"
|
|
50
|
+
description = "PostgreSQL chart for bow."
|
|
51
|
+
|
|
52
|
+
def render(self, values: dict[str, Any]) -> None:
|
|
53
|
+
name = values.get("name", self.name)
|
|
54
|
+
image = full_image(values.get("image", {}))
|
|
55
|
+
port = values.get("service", {}).get("port", 5432)
|
|
56
|
+
resources = values.get("resources")
|
|
57
|
+
architecture = values.get("architecture", "standalone")
|
|
58
|
+
persistence = values.get("persistence", {})
|
|
59
|
+
|
|
60
|
+
if architecture == "replication":
|
|
61
|
+
with StatefulSet(name, replicas=values.get("replicas", 1), service_name=f"{name}-headless"):
|
|
62
|
+
with postgresql_container(name, image, port, resources=resources):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
if persistence.get("enabled", True):
|
|
66
|
+
PersistentVolumeClaim("data", size=persistence.get("size", "8Gi"))
|
|
67
|
+
|
|
68
|
+
Service(port=port, type=values.get("service", {}).get("type", "ClusterIP"))
|
|
69
|
+
else:
|
|
70
|
+
with Deployment(name, replicas=values.get("replicas", 1)):
|
|
71
|
+
with postgresql_container(name, image, port, resources=resources):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
if persistence.get("enabled", True):
|
|
75
|
+
PersistentVolumeClaim("data", size=persistence.get("size", "8Gi"))
|
|
76
|
+
|
|
77
|
+
Service(port=port, type=values.get("service", {}).get("type", "ClusterIP"))
|
|
@@ -16,7 +16,7 @@ from bow.core.stack import _reset
|
|
|
16
16
|
from bow.chart.values import deep_merge, parse_set_values, merge_all_values
|
|
17
17
|
from bow.chart.registry import register_chart, get_chart, list_charts, reset_registry
|
|
18
18
|
from bow.chart.dependency import ChartDep, resolve_condition, get_dep_values
|
|
19
|
-
from
|
|
19
|
+
from postgresql import PostgreSQLChart
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@pytest.fixture(autouse=True)
|
|
@@ -13,9 +13,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
|
13
13
|
|
|
14
14
|
from bow.core.stack import _reset
|
|
15
15
|
from bow.chart.registry import register_chart, get_chart, reset_registry
|
|
16
|
-
from
|
|
17
|
-
from bow_redis import RedisChart, redis_container
|
|
18
|
-
from bow_redmine import RedmineChart, redmine_container, redmine_ingress
|
|
16
|
+
from postgresql import PostgreSQLChart, postgresql_container
|
|
19
17
|
from bow.core.manifest import manifest
|
|
20
18
|
from bow.core.resources import (
|
|
21
19
|
Deployment, Container, Service, Ingress,
|
|
@@ -28,8 +26,6 @@ def clean():
|
|
|
28
26
|
_reset()
|
|
29
27
|
reset_registry()
|
|
30
28
|
register_chart(PostgreSQLChart)
|
|
31
|
-
register_chart(RedisChart)
|
|
32
|
-
register_chart(RedmineChart)
|
|
33
29
|
yield
|
|
34
30
|
_reset()
|
|
35
31
|
reset_registry()
|
|
@@ -148,7 +144,7 @@ class TestComponentComposition:
|
|
|
148
144
|
"""pg_container can be used directly."""
|
|
149
145
|
with manifest() as m:
|
|
150
146
|
with Deployment("custom-db"):
|
|
151
|
-
with
|
|
147
|
+
with postgresql_container(database="custom_db", image="postgres:15"):
|
|
152
148
|
EnvVar("EXTRA_CONFIG", "value") # Extend
|
|
153
149
|
Service(port=5432)
|
|
154
150
|
|
|
@@ -158,28 +154,6 @@ class TestComponentComposition:
|
|
|
158
154
|
assert "POSTGRES_DB" in env_names
|
|
159
155
|
assert "EXTRA_CONFIG" in env_names # extended with `with`
|
|
160
156
|
|
|
161
|
-
def test_redis_container_standalone(self):
|
|
162
|
-
"""redis_container can be used directly."""
|
|
163
|
-
with manifest() as m:
|
|
164
|
-
with Deployment("cache"):
|
|
165
|
-
with redis_container(name="cache", image="redis:6"):
|
|
166
|
-
EnvVar("CUSTOM", "yes")
|
|
167
|
-
Service(port=6379)
|
|
168
|
-
|
|
169
|
-
dep = m.to_dicts()[0]
|
|
170
|
-
c = dep["spec"]["template"]["spec"]["containers"][0]
|
|
171
|
-
assert c["image"] == "redis:6"
|
|
172
|
-
env_names = [e["name"] for e in c.get("env", [])]
|
|
173
|
-
assert "CUSTOM" in env_names
|
|
174
|
-
|
|
175
|
-
def test_redmine_container_extend(self):
|
|
176
|
-
"""redmine_container can be extended."""
|
|
177
|
-
with manifest() as m:
|
|
178
|
-
with Deployment("my-redmine"):
|
|
179
|
-
with redmine_container(db_host="external-pg"):
|
|
180
|
-
EnvVar("REDMINE_PLUGINS_MIGRATE", "true")
|
|
181
|
-
Service(port=3000)
|
|
182
|
-
|
|
183
157
|
dep = m.to_dicts()[0]
|
|
184
158
|
c = dep["spec"]["template"]["spec"]["containers"][0]
|
|
185
159
|
env_names = [e["name"] for e in c["env"]]
|
|
@@ -190,7 +164,7 @@ class TestComponentComposition:
|
|
|
190
164
|
"""Different chart components used together."""
|
|
191
165
|
with manifest() as m:
|
|
192
166
|
with Deployment("pg"):
|
|
193
|
-
with
|
|
167
|
+
with postgresql_container(database="app"):
|
|
194
168
|
pass
|
|
195
169
|
Service(port=5432)
|
|
196
170
|
|
|
@@ -214,10 +188,9 @@ class TestComponentComposition:
|
|
|
214
188
|
"""pg_service component in multi-port mode."""
|
|
215
189
|
with manifest() as m:
|
|
216
190
|
with Deployment("pg"):
|
|
217
|
-
with
|
|
218
|
-
pass
|
|
219
|
-
with pg_service(metrics=True):
|
|
191
|
+
with postgresql_container():
|
|
220
192
|
pass
|
|
193
|
+
Service(port=5432, name="pg")
|
|
221
194
|
|
|
222
195
|
docs = m.to_dicts()
|
|
223
196
|
svc = [d for d in docs if d["kind"] == "Service"][0]
|
|
@@ -226,12 +199,4 @@ class TestComponentComposition:
|
|
|
226
199
|
assert "pg" in port_names
|
|
227
200
|
assert "metrics" in port_names
|
|
228
201
|
|
|
229
|
-
def test_ingress_component_extend(self):
|
|
230
|
-
"""redmine_ingress can be extended."""
|
|
231
|
-
with manifest() as m:
|
|
232
|
-
with redmine_ingress(host="app.example.com", tls=True):
|
|
233
|
-
IngressRule("/api", "api-service", 8080)
|
|
234
202
|
|
|
235
|
-
ing = m.to_dicts()[0]
|
|
236
|
-
paths = ing["spec"]["rules"][0]["http"]["paths"]
|
|
237
|
-
assert len(paths) == 2 # / + /api
|
|
@@ -16,7 +16,7 @@ from click.testing import CliRunner
|
|
|
16
16
|
|
|
17
17
|
from bow.core.stack import _reset
|
|
18
18
|
from bow.chart.registry import register_chart, reset_registry
|
|
19
|
-
from
|
|
19
|
+
from postgresql import PostgreSQLChart
|
|
20
20
|
from bow.cli import main
|
|
21
21
|
|
|
22
22
|
|
|
@@ -14,7 +14,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
|
14
14
|
|
|
15
15
|
from bow.core.stack import _reset
|
|
16
16
|
from bow.chart.registry import register_chart, reset_registry
|
|
17
|
-
from
|
|
17
|
+
from postgresql import PostgreSQLChart
|
|
18
18
|
from bow.stack.parser import parse_stack_file, parse_stack_dict, StackParseError
|
|
19
19
|
from bow.stack.refs import resolve_refs, RefError
|
|
20
20
|
from bow.stack.merger import merge_stack_files, apply_set_to_stack
|
|
@@ -16,9 +16,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
|
16
16
|
|
|
17
17
|
from bow.core.stack import _reset
|
|
18
18
|
from bow.chart.registry import register_chart, reset_registry
|
|
19
|
-
from
|
|
20
|
-
from bow_redis import RedisChart
|
|
21
|
-
from bow_redmine import RedmineChart
|
|
19
|
+
from postgresql import PostgreSQLChart
|
|
22
20
|
from bow.workspace.lock import (
|
|
23
21
|
LockSpec, parse_lock, write_lock, compute_checksum, check_drift, LockError,
|
|
24
22
|
)
|
|
@@ -31,8 +29,6 @@ def clean():
|
|
|
31
29
|
_reset()
|
|
32
30
|
reset_registry()
|
|
33
31
|
register_chart(PostgreSQLChart)
|
|
34
|
-
register_chart(RedisChart)
|
|
35
|
-
register_chart(RedmineChart)
|
|
36
32
|
yield
|
|
37
33
|
_reset()
|
|
38
34
|
reset_registry()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|