Skip to content

fix: handle metrics-endpoint binding and APIError on k8s - #133

Merged
gfouillet merged 5 commits into
juju:4.0from
gfouillet:gh130-metrics-endpoint-k8s-bind-fqdn
Aug 27, 2026
Merged

fix: handle metrics-endpoint binding and APIError on k8s#133
gfouillet merged 5 commits into
juju:4.0from
gfouillet:gh130-metrics-endpoint-k8s-bind-fqdn

Conversation

@gfouillet

@gfouillet gfouillet commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #130, Fixes #112. Related to #93.
JIRA: JUJU-10266

Fixes three issues affecting the metrics-endpoint relation hooks on k8s and HA controllers:

  1. controlsocket.APIError was never imported (Fixes metrics-endpoint-relation-created on every unit fail in cluster mode #112):
    The charm caught controlsocket.APIError, but APIError lives in unixsocket.py. Any socket error (e.g., user already exists, not found) crashed the hook with AttributeError: module 'controlsocket' has no attribute 'APIError'.

  2. _ensure_metrics_user recovery (Fixes metrics-endpoint-relation-created on every unit fail in cluster mode #112):
    The charm only handled HTTP 409; the Juju controller returns HTTP 500 (password destroyed) when AddUser consumes and zeroes the password buffer before checking UserAlreadyExists. The charm now catches both 409 and 500, removes the stale user, and recreates it cleanly.

  3. K8s service FQDN bind_address handling (Fixes metrics-endpoint-relation-created hook fails with ValueError on k8s when bind_address returns service FQDN #130, Related to Wrong host address for metrics scrape job #93):
    On k8s, network-get can return a service FQDN instead of an IP for bind_address early in the unit lifecycle. The prometheus_scrape library calls ipaddress.ip_network() on this value, raising ValueError. The charm now catches this, logs a warning, and defers the event so it retries when the binding resolves to a valid IP.

Fly-by fixes

  • controlsocket.APIError re-export: Added from unixsocket import APIError to controlsocket.py so the existing except controlsocket.APIError in charm.py actually works. The charm now uses controlsocket.APIError consistently rather than importing unixsocket directly, keeping controlsocket as the public interface.
  • Test coverage additions: Added negative-path tests for the error-whitelist re-raise branches (_ensure_metrics_user non-(409,500), _remove_metrics_user non-404), recovery retry failure propagation, repeated deferral until binding resolves, credential stability across deferred retry, and controlsocket.APIError re-export identity.

Related Juju PR

The Juju-side fix for the password destroyed root cause is in juju/juju PR #23143.

QA steps

End-to-end validation with companion PR:

  1. Bootstrap a controller on microk8s with the agent built from juju/juju PR #23143.
  2. On the deployed controller, copy src/controlsocket.py and src/charm.py from this PR.
  3. Run the reproduce steps from juju/juju-controller #130:
    juju offer controller.controller:metrics-endpoint
    juju deploy prometheus-k8s --channel 1/stable --trust
    juju relate prometheus-k8s admin/controller.controller
  4. Verify: The controller SAAS stays active after ~5 s (not error), and check_prometheus_targets passes without retry timeout.

Testing

  • ./run_tests — 43 unit tests pass (including error recovery for 409, 500, and 404, negative-path re-raise, retry failure propagation, repeated deferral, credential stability, and re-export identity).
  • flake8 — clean.
  • CI microk8s integration leg is the canonical signal.

Fix three issues that cause the metrics-endpoint relation hooks to fail
on k8s controllers (JUJU-10266):

1. controlsocket.APIError was never imported — the charm caught
   controlsocket.APIError but APIError lives in unixsocket. Any socket
   error crashed the hook with AttributeError.

2. _ensure_metrics_user only handled HTTP 409; the Juju controller
   returns HTTP 500 (password destroyed) when the user already exists.
   Now catches both 409 and 500, removes the stale user, and recreates
   it cleanly.

3. On k8s, network-get can return a service FQDN instead of an IP for
   bind_address early in the unit lifecycle. The prometheus_scrape
   library calls ipaddress.ip_network() on this value, raising
   ValueError. The charm now catches this, logs a warning, and defers
   the event so it retries when the binding resolves to an IP.

Juju-side fix: https://warthogs.atlassian.net/browse/JUJU-10266
@gfouillet
gfouillet force-pushed the gh130-metrics-endpoint-k8s-bind-fqdn branch from b1012aa to 780fbc3 Compare August 26, 2026 15:14
Add two tests covering the untested re-raise branches introduced in
PR juju#133:

- test_metrics_ensure_user_reraises_non_recoverable_error: verifies
  that an APIError with a code not in (409, 500) is re-raised without
  attempting the remove/recreate recovery.
- test_metrics_remove_user_reraises_non_404_error: verifies that a
  non-404 APIError from remove_metrics_user propagates during the
  409-recovery flow.
charm.py now catches controlsocket.APIError (via the re-export added
in the previous commit) rather than importing unixsocket directly.
This keeps charm.py depending on controlsocket as its public interface
and avoids a redundant direct dependency on the underlying socket
module. Tests updated to import APIError from controlsocket for
consistency.
Strengthen test_metrics_endpoint_update_scrape_job_spec_binding_unresolved
by asserting update_scrape_job_spec was actually called after the
ValueError, rather than only verifying no exception escaped.
Add four tests covering previously untested scenarios:

- test_metrics_ensure_user_retry_failure_propagates: verifies that
  when both add_metrics_user calls fail with 500, the second error
  propagates and remove_metrics_user is called exactly once (Mn1).
- test_metrics_endpoint_redefers_until_resolved: verifies that the
  charm keeps deferring while the binding is unresolved and succeeds
  once it resolves (Mn4).
- test_metrics_endpoint_password_stable_across_deferred_retry: verifies
  that the metrics password does not churn across a deferred retry
  even when generate_password would return a different value (Mn5).
- test_controlsocket_reexports_api_error: verifies that
  controlsocket.APIError is unixsocket.APIError (I3).
@gfouillet
gfouillet marked this pull request as ready for review August 27, 2026 09:40
jujubot added a commit to juju/juju that referenced this pull request Aug 27, 2026
…lure

#23143

## Description

When `AddUser` returns `UserAlreadyExists`, the `auth.Password` passed to it has already been hashed and destroyed by `auth.HashPassword` (which calls `p.Destroy()` via `defer`). Reusing that same password instance for the subsequent `GetUserByAuth` call results in `ErrPasswordDestroyed` and an HTTP 500 response, breaking the idempotency of `POST /metrics-users`.

`addMetricsUser` now accepts the raw password string and constructs separate `auth.Password` instances for `AddUser` and `GetUserByAuth`.

Also adds `--no-prompt` to `remove-application` calls in the prometheus controller charm test suite to avoid interactive prompts in CI.

## Root cause

In `internal/worker/controlsocket/worker.go`, `addMetricsUser` took a single `auth.Password`:
```go
_, _, err = w.accessService.AddUser(ctx, service.AddUserArg{
 Password: &password, // AddUser hashes and destroys this
 ...
})
if internalerrors.Is(err, usererrors.UserAlreadyExists) {
 user, err := w.accessService.GetUserByAuth(ctx, validatedName, password)
 // password is destroyed here -> ErrPasswordDestroyed -> HTTP 500
}
```

The existing unit tests did not catch this because `accessService` was mocked with GoMock and did not execute the real `HashPassword` which zeroes the bytes.

## Fix

```go
addPassword := auth.NewPassword(rawPassword)
_, _, err = w.accessService.AddUser(ctx, service.AddUserArg{
 Password: &addPassword,
 ...
})
if internalerrors.Is(err, usererrors.UserAlreadyExists) {
 authPassword := auth.NewPassword(rawPassword) // fresh instance
 user, err := w.accessService.GetUserByAuth(ctx, validatedName, authPassword)
}
```

## Related controller charm PR

The juju-controller charm-side fix is in **[juju/juju-controller PR #133](juju/juju-controller#133.

## Testing

- `go test ./internal/worker/controlsocket/...` — 24 tests pass (including new `TestMetricsUsersAddAlreadyExistsPasswordNotDestroyed` which simulates `AddUser` destroying the password via `DoAndReturn`).

## QA steps

**End-to-end validation with companion PR:**

1. Bootstrap a controller on microk8s with the agent built from this PR.
2. On the deployed controller, copy `src/controlsocket.py` and `src/charm.py` from [juju/juju-controller PR #133](juju/juju-controller#133).
3. Run the reproduce steps from [juju/juju-controller #130](juju/juju-controller#130):
 ```bash
 juju offer controller.controller:metrics-endpoint
 juju deploy prometheus-k8s --channel 1/stable --trust
 juju relate prometheus-k8s admin/controller.controller
 ```
4. **Verify:** The controller SAAS stays `active` after ~5 s (not `error`), and `check_prometheus_targets` passes without retry timeout.

## JIRA

[JUJU-10266](https://warthogs.atlassian.net/browse/JUJU-10266)

[JUJU-10266]: https://warthogs.atlassian.net/browse/JUJU-10266?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
@gfouillet
gfouillet merged commit 6f53c32 into juju:4.0 Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants