Add generic HaloPSA adapter#284
Draft
maximelb wants to merge 3 commits into
Draft
Conversation
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>
lcbill
approved these changes
Jul 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
halopsaUSP 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
tenantparameter) and self-hosted deployments. The authorisation server (/auth) and resource server (/api) are derived frominstance_url, or can each be overridden viaauth_url/api_url.endpointselects the API resource (defaultAudit). The records array is auto-detected from the response envelope ({record_count, <resource>: [...]}) or set explicitly withdata_field. Bare-array responses are also handled.id_field, defaultid). 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.extra_paramsinjects arbitrary static query string parameters per endpoint (e.g.extra_params.excludesys=true).401forces a token refresh.Configuration
instance_urlhttps://example.halopsa.com)auth_url/api_urlinstance_urlclient_id/client_secrettenantscopeallendpointAuditdata_fieldid_fieldidextra_paramspage_size100poll_interval60* or both
auth_urlandapi_url.Testing
go build ./containers/general— OKgo vet ./halopsa/ ./containers/...— OKgo test ./halopsa/(incl.-race) — OKUnit 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
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.client_options.mapping.event_time_path(e.g.datefor the audit log) so LimaCharlie uses each record's own timestamp.🤖 Generated with Claude Code