Skip to content

Add generic HaloPSA adapter#284

Draft
maximelb wants to merge 3 commits into
masterfrom
add-halopsa-adapter
Draft

Add generic HaloPSA adapter#284
maximelb wants to merge 3 commits into
masterfrom
add-halopsa-adapter

Conversation

@maximelb

Copy link
Copy Markdown
Contributor

Summary

Adds a new halopsa USP adapter that pulls events from HaloPSA into LimaCharlie in as close to their original format as possible.

The first target use case is audit log ingestion, but the adapter is intentionally generic — supporting a new HaloPSA event type only requires configuration changes, not code.

How it works

  • Authentication — HaloPSA OAuth2 client credentials flow. Works for hosted instances (optional tenant parameter) and self-hosted deployments. The authorisation server (/auth) and resource server (/api) are derived from instance_url, or can each be overridden via auth_url / api_url.
  • Generic endpoint pollingendpoint selects the API resource (default Audit). The records array is auto-detected from the response envelope ({record_count, <resource>: [...]}) or set explicitly with data_field. Bare-array responses are also handled.
  • Reliable incremental ingestion — uses a monotonic id high-water-mark (id_field, default id). Pages are walked newest-first and paging stops as soon as an already-seen record is reached, so steady-state polling is a single request. Records that shift between pages while new ones are inserted are de-duplicated within a poll. The first poll only establishes the cursor, so the adapter starts from "now" rather than replaying the full history.
  • Escape hatchextra_params injects arbitrary static query string parameters per endpoint (e.g. extra_params.excludesys=true).
  • Resilience — transient HTTP / token errors are logged and retried on the next poll; a 401 forces a token refresh.

Configuration

Key Description Default
instance_url Base HaloPSA URL (e.g. https://example.halopsa.com) required*
auth_url / api_url Override authorisation / resource server URLs derived from instance_url
client_id / client_secret HaloPSA API application credentials required
tenant Tenant identifier for some hosted deployments
scope OAuth2 scope all
endpoint API resource to poll Audit
data_field Response array field auto-detected
id_field Monotonic integer cursor field id
extra_params Extra static query parameters
page_size Records per page 100
poll_interval Seconds between polls 60

* or both auth_url and api_url.

Testing

  • go build ./containers/general — OK
  • go vet ./halopsa/ ./containers/... — OK
  • go test ./halopsa/ (incl. -race) — OK

Unit tests cover URL derivation (hosted/self-hosted/overrides/tenant), response parsing (envelope / explicit field / bare array / auto-detection / malformed), and the pagination + cursor logic (steady state, multi-page catch-up, page-shift de-dup, stop handling, error propagation).

Verified the adapter registers correctly: it appears in the CLI usage output and config validation reports missing required fields.

Notes / follow-ups

  • HaloPSA's Swagger spec does not document query parameters for GET /Audit; the adapter uses the pagination/ordering parameters (pageinate, page_size, page_no, order, orderdesc) that are standard across HaloPSA list controllers. Left as draft pending a validation run against a live HaloPSA instance.
  • Recommend setting client_options.mapping.event_time_path (e.g. date for the audit log) so LimaCharlie uses each record's own timestamp.

🤖 Generated with Claude Code

maximelb and others added 3 commits May 22, 2026 08:15
Adds a new `halopsa` adapter type that polls a HaloPSA REST API list
endpoint and ships every record into LimaCharlie in its original JSON
shape.

The adapter is intentionally generic so that new event types only need
configuration changes, not code:

- Authenticates with the HaloPSA OAuth2 client credentials flow, with
  support for hosted (tenant parameter) and self-hosted deployments;
  the authorisation/resource server URLs are derived from `instance_url`
  or can be overridden individually.
- Polls any list endpoint (`endpoint`, default `Audit` for audit logs),
  auto-detecting the response data array or using an explicit
  `data_field`.
- Incremental ingestion via a monotonic id high-water-mark (`id_field`):
  newest-first paging stops as soon as an already-seen record is reached,
  with intra-poll dedup for records that shift between pages. The first
  poll only establishes the cursor so the adapter starts from "now".
- `extra_params` passes arbitrary static query parameters per endpoint.
- Resilient polling: transient HTTP/token failures are logged and
  retried; a 401 forces a token refresh.

Includes unit tests for URL derivation, response parsing (envelope /
bare array / auto-detection) and the pagination + cursor logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLI/env parser auto-types each value, so `extra_params.excludesys=true`
becomes a bool and `extra_params.user_id=42` becomes an int — which then
failed to unmarshal into the previous `map[string]string`. Same problem
when the same values come from YAML.

Switch ExtraParams to `map[string]interface{}` and stringify each value
when assembling the request, so bools, numbers and strings can all be used
transparently from YAML or the CLI. Add a unit test covering the
stringification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds httptest-based mock tests covering the OAuth2 + paginated REST flow
the adapter exercises against real HaloPSA tenants: token fetch and
caching, token refresh on expiry/401, the {"<resource>": [...]} and
{"results": [...]} envelopes, explicit data_field, extra_params
propagation, bare-array responses, the baseline-then-incremental poll
lifecycle, multi-page catch-up, transient API errors preserving the
cursor, and page-beyond-end handling.

Also adds a build-tagged (-tags halopsa_live) test file that runs the
same code paths against a real HaloPSA instance via env vars, for
future regression checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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