Coverage for gco/services/aws_ssm.py: 100%
30 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
1"""
2SSM Parameter Store helpers shared across ``cli/``, ``mcp/`` and ``gco/services/``.
4Four free functions cover every shape callers in the tree currently
5reach for:
7* :func:`get_ssm_parameter` — fetch a value, propagate errors verbatim.
8 Use when the parameter is required and a missing parameter is a hard
9 failure. The :class:`botocore.exceptions.ClientError` raised by the
10 underlying ``ssm:GetParameter`` carries a ``ParameterNotFound`` code
11 that callers can match if they need to map missing into a friendlier
12 domain error.
13* :func:`get_ssm_parameter_optional` — fetch a value, return ``None`` on
14 the specific ``ParameterNotFound`` case while still propagating any
15 other error (permission denied, throttled, etc.). Use when a missing
16 parameter is a non-fatal "absent" signal but real errors should
17 surface.
18* :func:`check_ssm_parameter` — return ``(True, "")`` iff the parameter
19 exists, or ``(False, str(exc))`` on any error including
20 ``ParameterNotFound``. Use for diagnostics-style "is this thing here?"
21 checks where a transport error is treated the same as absent.
22* :func:`put_ssm_parameter` — write a parameter. Errors propagate
23 verbatim. Use for the rare cases (e.g. ALB hostname drift correction
24 in ``gco/services/health_monitor.py``) where the CLI / monitor needs
25 to write back to SSM.
27Architectural rationale. ``mcp/`` is forbidden from importing ``cli/``
28directly (the runtime tool surface shells out via subprocess instead),
29but ``mcp/`` already imports ``gco/services/...`` for shared service
30helpers. Putting these helpers under ``gco/services/`` lets every
31concrete callsite — the CLI's :class:`cli.models.ModelManager`, the
32analytics helpers in :mod:`cli.analytics_user_mgmt`, the
33``HealthMonitor`` in :mod:`gco.services.health_monitor`, and the
34:class:`mcp.mission.state.DynamoDBBackend` — share one implementation
35without re-introducing the ``mcp -> cli`` import edge.
37Each function lazy-imports ``boto3`` so the helper module's import
38surface stays free of SDK dependencies; tests that don't exercise SSM
39can monkeypatch ``boto3.client`` without dragging the whole CLI in.
40The same lazy-import pattern is already used in
41:func:`cli.analytics_user_mgmt.check_ssm_parameter` and the
42:class:`mcp.mission.state.DynamoDBBackend` — this module just
43consolidates it.
44"""
46from __future__ import annotations
48from typing import Any
50__all__ = [
51 "check_ssm_parameter",
52 "get_ssm_parameter",
53 "get_ssm_parameter_optional",
54 "put_ssm_parameter",
55]
58def _ssm_client(region: str | None) -> Any:
59 """Construct a fresh ``ssm`` boto3 client.
61 ``boto3`` is imported inside the function so a caller that monkey-
62 patches ``boto3.client`` for a test never has to also intercept a
63 module-level cached client. The fresh client is also free of
64 cross-test leakage: a permission-denied response in one test does
65 not poison the credential cache for the next.
66 """
67 import boto3
69 if region is None:
70 return boto3.client("ssm")
71 return boto3.client("ssm", region_name=region)
74def get_ssm_parameter(name: str, *, region: str | None = None) -> str:
75 """Fetch an SSM parameter value; propagate errors verbatim.
77 Args:
78 name: Fully-qualified parameter name (e.g. ``"/gco/foo"``).
79 region: Optional AWS region. ``None`` lets boto3's default
80 chain (env var, config file, instance metadata) decide.
82 Returns:
83 The parameter's ``Value`` field as a string.
85 Raises:
86 botocore.exceptions.ClientError: ``ParameterNotFound`` when the
87 parameter is missing, plus any other boto3 client error
88 (throttled, access denied, etc.).
89 botocore.exceptions.BotoCoreError: For transport / credential
90 failures.
92 The function does not catch any exception — errors carry the
93 underlying ``Code`` field that callers wanting domain-specific
94 messages can match on.
95 """
96 response = _ssm_client(region).get_parameter(Name=name)
97 return str(response["Parameter"]["Value"])
100def get_ssm_parameter_optional(name: str, *, region: str | None = None) -> str | None:
101 """Fetch an SSM parameter value or return ``None`` if absent.
103 Distinguishes the ``ParameterNotFound`` case (returns ``None``)
104 from every other error (re-raised verbatim). This matches the
105 pattern :class:`gco.services.health_monitor.HealthMonitor` reaches
106 for when it has to read-then-maybe-write a drift-tracker
107 parameter — a missing value is not an error, but a permission
108 denied or a throttle is.
110 Args:
111 name: Fully-qualified parameter name.
112 region: Optional AWS region.
114 Returns:
115 The parameter's ``Value`` as a string, or ``None`` if the
116 parameter does not exist.
118 Raises:
119 botocore.exceptions.ClientError: Any error other than
120 ``ParameterNotFound``.
121 botocore.exceptions.BotoCoreError: Transport / credential
122 failures.
123 """
124 from botocore.exceptions import ClientError
126 client = _ssm_client(region)
127 try:
128 response = client.get_parameter(Name=name)
129 except ClientError as exc:
130 # ``ParameterNotFound`` is the only error this helper translates
131 # to ``None``; every other code propagates so the caller sees
132 # the real failure.
133 if exc.response.get("Error", {}).get("Code") == "ParameterNotFound":
134 return None
135 raise
136 return str(response["Parameter"]["Value"])
139def check_ssm_parameter(name: str, *, region: str | None = None) -> tuple[bool, str]:
140 """Return ``(True, "")`` iff the parameter exists, ``(False, error)`` otherwise.
142 Diagnostic-style helper that flattens every kind of failure
143 (missing, throttled, denied, transport error) into a single
144 boolean. The returned error string is the underlying exception's
145 ``str()`` so a caller logging the result has the original message.
147 Args:
148 name: Fully-qualified parameter name.
149 region: Optional AWS region.
151 Returns:
152 ``(True, "")`` when the parameter resolves; ``(False, str(exc))``
153 on any error including ``ParameterNotFound``.
154 """
155 from botocore.exceptions import BotoCoreError, ClientError
157 try:
158 _ssm_client(region).get_parameter(Name=name)
159 except (ClientError, BotoCoreError) as exc:
160 return False, str(exc)
161 return True, ""
164def put_ssm_parameter(
165 name: str,
166 value: str,
167 *,
168 region: str | None = None,
169 parameter_type: str = "String",
170 overwrite: bool = True,
171) -> None:
172 """Write or overwrite an SSM parameter. Errors propagate verbatim.
174 Args:
175 name: Fully-qualified parameter name.
176 value: New string value to store.
177 region: Optional AWS region.
178 parameter_type: SSM parameter type — ``"String"`` (default),
179 ``"StringList"``, or ``"SecureString"``.
180 overwrite: When ``True`` (default), passes ``Overwrite=True``
181 to ``ssm:PutParameter`` so an existing parameter is
182 replaced. When ``False``, the call fails with
183 ``ParameterAlreadyExists`` if the name is taken.
185 Raises:
186 botocore.exceptions.ClientError: For SSM-side errors (insufficient
187 permission, parameter type mismatch, etc.).
188 botocore.exceptions.BotoCoreError: For transport / credential
189 failures.
191 The helper does not catch any exception — callers that need to
192 classify failures (e.g. retry on throttle) match on the
193 underlying ``Code`` field themselves.
194 """
195 _ssm_client(region).put_parameter(
196 Name=name,
197 Value=value,
198 Type=parameter_type,
199 Overwrite=overwrite,
200 )