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.
Files changed (86) hide show
  1. {bow_cli-0.4.2 → bow_cli-0.4.4}/PKG-INFO +1 -1
  2. {bow_cli-0.4.2 → bow_cli-0.4.4}/pyproject.toml +1 -1
  3. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/base.py +20 -27
  4. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/dependency.py +18 -10
  5. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/values.py +73 -4
  6. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/resources.py +75 -5
  7. bow_cli-0.4.4/tests/postgresql/__init__.py +77 -0
  8. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_chart.py +1 -1
  9. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_charts.py +5 -40
  10. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_cli.py +1 -1
  11. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_stack.py +1 -1
  12. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_workspace.py +1 -5
  13. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.analyze.agent.md +0 -0
  14. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.checklist.agent.md +0 -0
  15. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.clarify.agent.md +0 -0
  16. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.constitution.agent.md +0 -0
  17. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.implement.agent.md +0 -0
  18. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.plan.agent.md +0 -0
  19. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.specify.agent.md +0 -0
  20. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.tasks.agent.md +0 -0
  21. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/agents/speckit.taskstoissues.agent.md +0 -0
  22. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.analyze.prompt.md +0 -0
  23. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.checklist.prompt.md +0 -0
  24. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.clarify.prompt.md +0 -0
  25. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.constitution.prompt.md +0 -0
  26. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.implement.prompt.md +0 -0
  27. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.plan.prompt.md +0 -0
  28. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.specify.prompt.md +0 -0
  29. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.tasks.prompt.md +0 -0
  30. {bow_cli-0.4.2 → bow_cli-0.4.4}/.github/prompts/speckit.taskstoissues.prompt.md +0 -0
  31. {bow_cli-0.4.2 → bow_cli-0.4.4}/.gitignore +0 -0
  32. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/memory/constitution.md +0 -0
  33. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  34. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/common.sh +0 -0
  35. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/create-new-feature.sh +0 -0
  36. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/setup-plan.sh +0 -0
  37. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/scripts/bash/update-agent-context.sh +0 -0
  38. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/agent-file-template.md +0 -0
  39. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/checklist-template.md +0 -0
  40. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/constitution-template.md +0 -0
  41. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/plan-template.md +0 -0
  42. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/spec-template.md +0 -0
  43. {bow_cli-0.4.2 → bow_cli-0.4.4}/.specify/templates/tasks-template.md +0 -0
  44. {bow_cli-0.4.2 → bow_cli-0.4.4}/.vscode/launch.json +0 -0
  45. {bow_cli-0.4.2 → bow_cli-0.4.4}/.vscode/settings.json +0 -0
  46. {bow_cli-0.4.2 → bow_cli-0.4.4}/AGENTS.md +0 -0
  47. {bow_cli-0.4.2 → bow_cli-0.4.4}/README.md +0 -0
  48. {bow_cli-0.4.2 → bow_cli-0.4.4}/install.sh +0 -0
  49. {bow_cli-0.4.2 → bow_cli-0.4.4}/poetry.lock +0 -0
  50. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/__init__.py +0 -0
  51. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/__init__.py +0 -0
  52. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/chart/registry.py +0 -0
  53. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/__init__.py +0 -0
  54. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/env_cmd.py +0 -0
  55. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/inspect_cmd.py +0 -0
  56. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/install_cmd.py +0 -0
  57. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/list_cmd.py +0 -0
  58. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/lock_cmd.py +0 -0
  59. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/pull_cmd.py +0 -0
  60. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/push_cmd.py +0 -0
  61. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/registry_cmd.py +0 -0
  62. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/status_cmd.py +0 -0
  63. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/template.py +0 -0
  64. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/uninstall_cmd.py +0 -0
  65. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/cli/up.py +0 -0
  66. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/__init__.py +0 -0
  67. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/manifest.py +0 -0
  68. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/resource.py +0 -0
  69. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/core/stack.py +0 -0
  70. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/__init__.py +0 -0
  71. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/client.py +0 -0
  72. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/config.py +0 -0
  73. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/oci/env.py +0 -0
  74. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/__init__.py +0 -0
  75. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/engine.py +0 -0
  76. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/merger.py +0 -0
  77. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/parser.py +0 -0
  78. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/stack/refs.py +0 -0
  79. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/utils.py +0 -0
  80. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/__init__.py +0 -0
  81. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/lock.py +0 -0
  82. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/resolver.py +0 -0
  83. {bow_cli-0.4.2 → bow_cli-0.4.4}/src/bow/workspace/stage.py +0 -0
  84. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_core.py +0 -0
  85. {bow_cli-0.4.2 → bow_cli-0.4.4}/tests/test_oci.py +0 -0
  86. {bow_cli-0.4.2 → bow_cli-0.4.4}/uninstall.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bow-cli
3
- Version: 0.4.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bow-cli"
7
- version = "0.4.2"
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"
@@ -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
@@ -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 bow_postgresql import PostgreSQLChart
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 bow_postgresql import PostgreSQLChart, pg_container, pg_service
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 pg_container(database="custom_db", image="postgres:15"):
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 pg_container(database="app"):
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 pg_container():
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 bow_postgresql import PostgreSQLChart
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 bow_postgresql import PostgreSQLChart
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 bow_postgresql import PostgreSQLChart
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