- Go 99.7%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| docs | ||
| internal | ||
| scripts | ||
| .gitignore | ||
| .goreleaser.yml | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
| RELEASING.md | ||
| signing-key.asc | ||
| terraform-registry-manifest.json | ||
| VERSION | ||
terraform-provider-rustinion
A Terraform provider for managing
Rustinion monitoring configuration as code, built on
terraform-plugin-framework.
It wraps the customer-facing config-as-code API documented in
web-api/src/routes/customer_api.rs in the Rustinion repository — the same REST
surface a tenant's own read_write-scoped API key already gets, this
is just a Terraform-shaped client for it.
Resources
| Resource | Rustinion API |
|---|---|
rustinion_alert_rule |
/api/v1/customer/alert-rules[/:id] |
rustinion_notification_channel |
/api/v1/customer/notification-channels[/:id] |
rustinion_dashboard |
/api/v1/customer/dashboards[/:id] |
rustinion_synthetic_check |
/api/v1/customer/synthetic-checks[/:id] |
rustinion_log_alert_rule |
/api/v1/customer/log-alert-rules[/:id] |
rustinion_integration |
/api/v1/customer/integrations[/:id] |
rustinion_status_page |
/api/v1/customer/status-page (singleton — no :id) |
rustinion_escalation_policy |
/api/v1/customer/escalation-policies[/:id] |
rustinion_escalation_acknowledgement |
/api/v1/customer/escalations/:rule_kind/:rule_id/acknowledge |
rustinion_on_call_schedule |
/api/v1/customer/on-call-schedules[/:id] |
rustinion_on_call_override |
/api/v1/customer/on-call-schedules/:id/overrides[/:override_id] |
rustinion_group |
/api/v1/customer/groups[/:id] |
rustinion_group_portable_policy |
/api/v1/customer/groups/:id/portable (per-group singleton: portable by default, away/asleep grace, auto_mark_portable = suggest / auto / off for devices that look portable) |
rustinion_group_vm_snapshot_policy |
/api/v1/customer/groups/:id/vm-snapshot-policy (per-group singleton: snapshot a VM on its managed Proxmox VE / libvirt host before firmware and OS upgrades, on by default; Rustinion snapshots kept per VM, default 2) |
rustinion_group_restart_policy |
/api/v1/customer/groups/:id/restart-policy (per-group singleton: windows, rings, max at once, drain/verify hooks, ignored_services as policy_json) |
rustinion_group_alert_tuning |
/api/v1/customer/groups/:id/alert-tuning (per-group singleton) |
rustinion_group_offline_alerts |
/api/v1/customer/groups/:id/offline-alerts (per-group singleton) |
rustinion_group_notifications |
/api/v1/customer/groups/:id/notifications (per-group singleton: normal, digest_only = never pushed for any reason, still in the email digest, or silent) |
rustinion_group_host_health |
/api/v1/customer/groups/:id/host-health (per-group singleton) |
rustinion_group_build_host |
/api/v1/customer/groups/:id/build-host (per-group singleton: Build/CI host mode, CI job containers' web-process findings are info only; container_patterns) |
rustinion_group_terminal_services |
/api/v1/customer/groups/:id/terminal-services (per-group singleton: expected terminal services - browser terminals whose shells are info only; builtins from the suggested list, custom entries matched by program path and arguments) |
rustinion_group_file_integrity |
/api/v1/customer/groups/:id/file-integrity (per-group singleton: file integrity monitoring - built-in watch lists per OS, extra paths, exclusions, scan interval, urgent alert for critical system files; package-manager changes are recognised and never alert) |
rustinion_group_detection |
/api/v1/customer/groups/:id/detection (per-group singleton: Sigma-compatible detection rules over collected logs - on/off, minimum level for findings, urgent alert for critical matches, per-rule switch-off, level and threshold) |
rustinion_aws_security_account |
/api/v1/customer/aws/accounts (an AWS account whose CloudTrail events and GuardDuty findings are read through a read-only cross-account role with an external id; the resource exports external_id for the role's trust policy) |
rustinion_group_active_response |
/api/v1/customer/groups/:id/active-response (per-group singleton: active response to detection matches - dry run by default, block source address with TTL, kill process, lock new account, allowlist and protected accounts) |
rustinion_group_hardening |
/api/v1/customer/groups/:id/hardening (per-group singleton: CIS-style hardening benchmarks - shipped policies for Debian/Ubuntu, RHEL family, Windows, macOS and OpenWrt, scan interval, minimum score, accepted risks) |
rustinion_group_custom_metrics |
/api/v1/customer/groups/:id/custom-metrics (per-group singleton: the agent's DogStatsD/StatsD listener and Prometheus/OpenMetrics scrape targets) |
rustinion_alert_routing |
/api/v1/customer/alert-routing (organization singleton: severities per destination - email, digest webhook/ntfy, PagerDuty - ntfy priority per severity, digest hour and time zone; push destinations default to critical only) |
rustinion_group_package_cleanup |
/api/v1/customer/groups/:id/package-cleanup (per-group singleton: remove unneeded packages / old kernels, on by default) |
rustinion_group_log_collection |
/api/v1/customer/groups/:id/log-collection (per-group singleton: device log collection on/off, severity, Windows channels, app log files, containers, syslog receiver, rate limit and buffer as settings_json) |
rustinion_device_log_collection |
/api/v1/customer/devices/:id/log-collection (per-device override of the group's log collection, settings_json; null/destroy = inherit) |
rustinion_device_package_cleanup |
/api/v1/customer/devices/:id/package-cleanup (per-device on / off / inherit) |
rustinion_device_agent_update |
/api/v1/customer/devices/:id/agent-update (one device's agent self-update: follow_group, auto, paused, or pinned + pinned_version; destroy = follow_group) |
rustinion_group_malware_scan |
/api/v1/customer/groups/:id/malware-scan (per-group singleton: signatures-only and excluded scan paths) |
rustinion_malware_allowlist_rule |
/api/v1/customer/malware-allowlist (fleet-wide false positive: signature + path pattern) |
rustinion_slo |
/api/v1/customer/slos[/:id] |
rustinion_silence |
/api/v1/customer/silences[/:id] |
rustinion_snmp_target |
/api/v1/customer/groups/:id/snmp-targets, /api/v1/customer/snmp-targets/:id (community / v3 keys write-only) |
rustinion_device_tag |
/api/v1/customer/devices/:id/tags[/:tag] (one tag on one device) |
rustinion_maintenance_window |
/api/v1/customer/maintenance-windows[/:id] (no update; destroy cancels) |
rustinion_rum_application |
/api/v1/customer/rum-applications[/:id] (exports the browser client_token) |
rustinion_report_schedule |
/api/v1/customer/report-schedules[/:id] |
rustinion_user |
/api/v1/customer/users[/:id] |
rustinion_permission_grant |
/api/v1/customer/permission-grants[/:id] |
rustinion_permission_role |
/api/v1/customer/permission-roles[/:id] |
rustinion_permission_role_assignment |
/api/v1/customer/permission-roles/:role_id/assignments[/:assignment_id] |
rustinion_permission_role_mapping |
/api/v1/customer/permission-role-mappings[/:id] |
Every resource supports create/read/update/delete and terraform import <resource>.<name> <id> (the server-assigned UUID) — with a few
exceptions:
- The
rustinion_group_*settings resources (portable policy, restart policy, alert tuning, offline alerts, notifications, host health, custom metrics, VM snapshot policy) are per-group singletons: their id and import id are the group id, creating one writes the settings, and destroying one puts the server defaults back. rustinion_status_pageis a singleton (exactly one status page per tenant): its import id is a placeholder, never looked up, sinceGET /api/v1/customer/status-pagetakes no id at all (see that resource's own doc comment).rustinion_on_call_overridehas no in-place update at all (every attribute isRequiresReplace— the server's own API is create/delete-only for an override, matching the admin UI's own separate add/remove actions) and its import id isschedule_id/ override_id, not a bare id, since an override is only ever readable through its parent schedule's ownGET(see that resource's own doc comment).rustinion_grouphas no rename or freeform manifest edit anywhere in this app (admin UI included) —nameis fixed at creation, andPUTonly ever (re-)applies a startertemplate_key, which this resource updates in place.template_keyis never returned by the API on read, so this provider deliberately preserves whatever value you last set instead of re-deriving (and fighting you over) one the server can't tell it. Deleting a group only succeeds if it has no devices and no outstanding enrollment tokens — the same database constraints that protect it everywhere else in this app, surfaced as a real Terraform error, not silently ignored, if it still has either.rustinion_sloandrustinion_silencehave no in-place update at all — every attribute isRequiresReplace, matching neither API having an update endpoint (both are create/read/delete-or-cancel only, mirroring their own admin UIs, which have never had an edit path for an existing SLO or silence either). Destroying arustinion_silencecancels it server-side (cancelled_at = now()) rather than deleting its row, matching the admin UI's own "cancel" action.rustinion_permission_granthas noPUTendpoint behind it, but still updatespermission_levelin place: a grant's identity is its(user_id, group_id, resource_type)tuple (enforced server-side by two partial unique indexes), so those three areRequiresReplaceand the only mutable field is the level — whichPOSTalready changes, upserting on that same scope and returning the same id. That is deliberate rather than incidental: Terraform would otherwise destroy-then-create on a level change, and for areadgrant that restricts an admin, the window between the two would silently restore their unrestricted access. Reference a user withrustinion_user.example.user_id, not.id— the latter is that resource'suser_rolesrow id, while a grant is keyed on the user identity. A grant for someone with no role in the tenant is rejected (a grant refines access, it never confers membership).rustinion_permission_role_assignmenthas no in-place update at all (every attribute isRequiresReplace) and its import id isrole_id/assignment_id, not a bare id — an assignment has no mutable state, it is its(role, user, scope)tuple, and it is only ever readable through its parent role's own list endpoint. Destroying the parentrustinion_permission_rolecascades its assignments away server-side, so a plan that removes both at once is not an error.rustinion_permission_role_mappinghas no in-place update either — a mapping has no mutable state at all, it is its(source, external_group, role_id, group_id)tuple, so every attribute isRequiresReplaceand the API has noPUT.sourceis one ofscim,oidcorsaml. Destroying one does not immediately revoke the role assignments it produced: those are removed on each affected user's next sync (a SCIM group write, or that user's next OIDC/SAML sign-in). That is deliberate server-side behavior rather than a provider limitation — revoking access outside a sync would do it at a moment nobody is watching and with no audit entry for the run.rustinion_escalation_acknowledgementis not config-as-code at all — it models a one-shot action (acknowledging a firing escalation), not persistent server-side state.rule_kind/rule_idareRequiresReplace;Readdrops the resource from state the moment the acknowledgement it recorded no longer holds (the rule cleared and fired again, starting a fresh unacknowledged episode), so a laterapplynaturally re-acknowledges instead of going stale silently.Deleteis a no-op — there is no server-side "un-acknowledge."
PUT on the Rustinion side is a full resource replace, not a partial
patch — see each resource's own doc comment in internal/provider/ —
so this provider's Update always sends every field, matching the
plan's full desired state, same as terraform apply always does.
rustinion_integration is one exception: its PUT only ever updates
config (kind/group_id are fixed at install time — see that
resource's own doc comment), which is why both are RequiresReplace in
its schema. rustinion_escalation_policy's step block fans out to
every channel in its channel_ids list independently — a missing/
disabled channel or a failed delivery on one never blocks the others.
rustinion_on_call_schedule never manages overrides (a separate
resource — see above) — see that resource's own doc comment for why.
Requirements
- Go 1.23+ (to build the provider)
- Terraform CLI 1.5+ (to actually run
terraform plan/applyagainst it, or to run this provider's acceptance tests, which shell out to it) - A running Rustinion instance and a
read_write-scoped API key for one of its tenants (Tenant admin panel -> API keys -> Read + write)
Building
go build -o terraform-provider-rustinion .
Installing
Published on the public Terraform Registry as
CanuteTheGreat/rustinion:
terraform {
required_providers {
rustinion = {
source = "CanuteTheGreat/rustinion"
version = "~> 0.1"
}
}
}
terraform init downloads it and verifies the release's GPG-signed
checksums (key 4F1D 6FD9 4A22 32E1 58AE 21DD 642A E4F0 4B94 C1F1,
"Errant Solutions (Rustinion Terraform provider releases)").
A local build instead
To try an unreleased change, point Terraform at a local build with a dev override:
# ~/.terraformrc
provider_installation {
dev_overrides {
"registry.terraform.io/CanuteTheGreat/rustinion" = "/absolute/path/to/terraform-provider-rustinion"
}
direct {}
}
While the override is active terraform init is not needed (Terraform
prints a reminder on every command; that is expected).
Authentication
provider "rustinion" {
base_url = "https://rustinion.com" # or the RUSTINION_BASE_URL env var
api_key = "..." # or the RUSTINION_API_KEY env var — sensitive
}
Both attributes are optional in the provider block and fall back to
RUSTINION_BASE_URL/RUSTINION_API_KEY environment variables — the
usual Terraform provider convention for credentials, so a real key
never has to live in checked-in .tf files. The key must be
read_write-scoped (0067_api_key_scope.sql in the main repo); a
read_only key is rejected by the API itself with a 403 on the very
first apply, surfaced as a normal Terraform error, not a provider
crash.
Example
# Alert content is customisable per channel. Omit these for the
# built-in message format.
resource "rustinion_notification_channel" "ops_webhook" {
name = "Internal incident API"
channel_type = "webhook"
config = jsonencode({ url = "https://ops.example.com/incidents" })
# {{variable}} substitution over the finding that fired. A webhook's
# payload_template replaces the default {"subject","body","severity"}
# envelope entirely, which is what makes it possible to POST the exact
# shape a receiving system expects.
subject_template = "[{{severity}}] {{reason}}"
payload_template = jsonencode({
title = "{{reason}} on {{hostname}}"
detail = "{{detail}}"
})
}
resource "rustinion_notification_channel" "ops_slack" {
name = "ops-slack"
channel_type = "slack"
config = jsonencode({ webhook_url = "https://hooks.slack.com/services/..." })
}
resource "rustinion_alert_rule" "high_cpu" {
group_id = var.group_id
name = "High CPU"
severity = "warning"
condition {
metric_type = "cpu_avg_pct"
comparator = "gt"
threshold = 90
}
}
# A forecast condition alerts before disk_avg_pct actually crosses 90 -
# once its current linear trend is projected to reach it within
# forecast_within_hours (Datadog calls this a "Forecast Monitor").
resource "rustinion_alert_rule" "disk_capacity_forecast" {
group_id = var.group_id
name = "Disk filling up"
severity = "warning"
condition {
metric_type = "disk_avg_pct"
condition_kind = "forecast"
comparator = "gt"
threshold = 90
forecast_within_hours = 48
}
}
resource "rustinion_dashboard" "fleet_overview" {
group_id = var.group_id
name = "Fleet overview"
widget {
title = "CPU"
widget_type = "kpi_tile"
config = jsonencode({ metric_type = "cpu_avg_pct", window_minutes = 60 })
}
}
resource "rustinion_synthetic_check" "health" {
group_id = var.group_id
name = "Health check"
check_type = "http"
target = "https://example.com/health"
}
resource "rustinion_log_alert_rule" "ssh_failures" {
group_id = var.group_id
name = "SSH login failures"
log_severity = "error"
log_source = "journal"
}
resource "rustinion_integration" "nginx" {
kind = "nginx"
group_id = var.group_id
config = jsonencode({ target = "https://example.com/health" })
}
resource "rustinion_status_page" "public" {
slug = "status"
title = "Example status"
component {
synthetic_check_id = rustinion_synthetic_check.health.id
display_name = "API"
}
}
resource "rustinion_on_call_schedule" "primary" {
group_id = var.group_id
name = "Primary rotation"
rotation_days = 7
member {
user_id = var.oncall_user_id
}
}
resource "rustinion_on_call_override" "vacation_cover" {
schedule_id = rustinion_on_call_schedule.primary.id
user_id = var.covering_user_id
starts_at = "2026-07-01T00:00:00Z"
ends_at = "2026-07-08T00:00:00Z"
}
resource "rustinion_notification_channel" "ops_pagerduty" {
name = "ops-pagerduty"
channel_type = "pagerduty"
config = jsonencode({ integration_key = "..." })
# Paging channels (pagerduty, opsgenie, sms, voice, ntfy) get critical findings
# only unless widened; email/slack/webhook get every severity.
# severities = ["critical", "warning"]
}
# Emergencies to the phone, everything else in one daily email at 09:00.
resource "rustinion_alert_routing" "this" {
email_severities = ["warning", "info"]
webhook_severities = ["critical"] # e.g. an ntfy topic, set on the tenant page
ntfy_priority_critical = "urgent"
alert_digest_cadence = "daily"
summary_digest_cadence = "daily"
digest_hour = 9
digest_timezone = "America/Los_Angeles"
}
resource "rustinion_notification_channel" "ops_sms" {
name = "ops-sms"
channel_type = "sms"
config = jsonencode({
account_sid = var.twilio_account_sid
auth_token = var.twilio_auth_token
from_number = "+15551234567"
to_numbers = ["+15557654321"]
})
}
Every channel's config — including secrets like the Twilio auth_token
above, a Slack webhook URL, a PagerDuty routing key, or a webhook HMAC
secret — is now encrypted at rest server-side (AES-256-GCM, an
app-managed key, not just database access control). This is entirely
transparent to Terraform: the wire shape is unchanged (config is still
plain JSON in both the request Terraform sends and the response it reads
back to refresh state), so no schema or attribute changes were needed
here.
resource "rustinion_escalation_policy" "primary" {
group_id = var.group_id
name = "Primary escalation"
on_call_schedule_id = rustinion_on_call_schedule.primary.id
step {
# Fans out to both channels at once.
channel_ids = [rustinion_notification_channel.ops_slack.id, rustinion_notification_channel.ops_pagerduty.id]
wait_minutes = 15
}
}
resource "rustinion_alert_rule" "disk_full" {
group_id = var.group_id
name = "Disk almost full"
escalation_policy_id = rustinion_escalation_policy.primary.id
condition {
metric_type = "disk_avg_pct"
comparator = "gt"
threshold = 90
}
}
# Acknowledges disk_full's currently-firing escalation, if any — a
# one-shot action, not persistent config; see this resource's own
# section below.
resource "rustinion_escalation_acknowledgement" "ack_disk_full" {
rule_kind = "alert_rule"
rule_id = rustinion_alert_rule.disk_full.id
}
resource "rustinion_group" "web_servers" {
name = "Web servers"
template_key = "linux_web_server"
}
resource "rustinion_slo" "fleet_uptime" {
group_id = rustinion_group.web_servers.id
name = "Fleet uptime"
metric_type = "checkin_freshness"
target_percentage = 99.9
}
resource "rustinion_silence" "planned_maintenance" {
name = "Planned maintenance"
group_id = rustinion_group.web_servers.id
starts_at = "2026-07-01T02:00:00Z"
ends_at = "2026-07-01T04:00:00Z"
reason = "Scheduled DB upgrade"
}
resource "rustinion_user" "dept_admin" {
email = "ops-lead@example.com"
group_id = rustinion_group.web_servers.id
access_level = "admin"
}
# Fine-grained override: this user is read-only across the org (above),
# but may edit alert rules for the web servers group specifically.
resource "rustinion_permission_grant" "alert_rules_for_web" {
user_id = rustinion_user.dept_admin.user_id
group_id = rustinion_group.web_servers.id
resource_type = "alert_rules"
permission_level = "write"
}
# For anything you'd grant more than once, define it as a role instead
# and assign people to it — then change the role in one place.
resource "rustinion_permission_role" "sre" {
name = "SRE"
description = "On-call engineers"
permission {
resource_type = "alert_rules"
permission_level = "admin"
}
permission {
resource_type = "synthetic_checks"
permission_level = "write"
}
}
resource "rustinion_permission_role_assignment" "dept_admin_is_sre" {
role_id = rustinion_permission_role.sre.id
user_id = rustinion_user.dept_admin.user_id
# Omit group_id for a tenant-wide assignment.
}
# Better still: let the directory decide who is an SRE. Anyone whose
# OIDC token carries the "sre" group gets the role at sign-in, and
# loses it when they leave the group. Assignments made by hand (above)
# are never touched by a sync.
resource "rustinion_permission_role_mapping" "sre_from_idp" {
source = "oidc"
external_group = "sre"
role_id = rustinion_permission_role.sre.id
}
# Same thing for a SAML IdP — read from the assertion's group
# attribute instead of an OIDC claim. A tenant can run more than one
# source at once; each only ever manages its own assignments.
resource "rustinion_permission_role_mapping" "sre_from_saml" {
source = "saml"
external_group = "sre"
role_id = rustinion_permission_role.sre.id
}
See each resource's own schema (terraform providers schema -json, or the doc comments in internal/provider/resource_*.go) for
every attribute — condition/widget/step/component/member
nested blocks in particular, which mirror the Rust API's own nested
conditions/widgets/steps/components/members arrays field for
field.
Running acceptance tests
This provider's acceptance tests (internal/provider/*_test.go,
TestAcc*) are real, not mocked: they run actual terraform apply/
plan/destroy cycles (via terraform-plugin-testing, which drives
this provider's own code in-process — no separate provider binary
install needed) against a real, running rustinion-web instance
backed by a real Postgres. They're skipped by a plain go test ./...
(the standard Go convention this codebase's own Rust tests follow too:
acceptance-style tests are opt-in, not run by default) unless
TF_ACC=1 is set.
- Start the dev Postgres (from the main repo root):
docker compose up -d postgres - Run a real
rustinion-webagainst it, migrations included (from the main repo'sweb/directory):DATABASE_URL=postgres://rustinion:devpassword@localhost:5433/rustinion PORT=8090 cargo run --bin rustinion-web - Bootstrap a throwaway tenant, group,
read_writeAPI key, and one tenant member directly in that database (there's no signup/API-key-creation/user-invite flow reachable without an admin session and a browser):eval "$(DATABASE_URL=postgres://rustinion:devpassword@localhost:5433/rustinion ./scripts/setup_test_tenant.sh)" export RUSTINION_BASE_URL=http://localhost:8090 - Run the suite:
TF_ACC=1 go test ./internal/provider/... -run TestAcc -v
Every acceptance test creates and destroys its own resources; nothing
persists in the tenant setup_test_tenant.sh created beyond the test
run itself (Terraform's own CheckDestroy in each test confirms the
resource is actually gone server-side afterward, not just removed from
state).
The server's per-key rate limit (60 requests per rolling 60s,
web/src/rate_limit.rs's customer_api_key quota) applies here exactly
as in production. The provider waits out a 429 for the Retry-After
the server sends and retries (up to five minutes per call), so a full
TestAcc* run, like any large terraform apply, is slower once it drains
the bucket rather than failing partway through.
CI runs the whole acceptance suite on every push to main (the
live-e2e job in .forgejo/workflows/ci-cd.yml, via
scripts/ci/live-e2e.sh in the main repo) against a freshly built
rustinion-web and a throwaway Postgres.
The internal/client package additionally has plain, fast unit tests
(go test ./internal/client/..., no TF_ACC, no real server) covering
request construction, auth headers, and error/404 handling against an
httptest server — a cheap complement to, not a substitute for, the
real acceptance tests above.
Limitations
Honest, deliberate scope cuts — not oversights:
- Every resource the Rustinion customer API exposes is now covered
(alert rules, notification channels, dashboards, synthetic checks,
log alert rules, integrations, the tenant's one status page,
escalation policies, on-call schedules/overrides, groups, SLOs,
silences, and users).
rustinion_on_call_schedule's own acceptance test still provisions its test user ahead of time viascripts/setup_test_tenant.shrather than arustinion_userresource of its own — kept that way deliberately (see that script's own comment) sincerustinion_usersends a real password-set email on every create, which isn't a side effect an unrelated acceptance test for a different resource should trigger just to get a user id. - An escalation policy step notifies through exactly one channel —
the schema
escalation_stepsalready shipped with (see0051_on_call_and_escalation.sqlin the main repo); there's no fan-out to several channels at one escalation level. - No SMS/voice escalation channels —
notification_channelssupports email/Slack/webhook/PagerDuty/Opsgenie today; an escalation step (or any notification channel) can only ever notify through one of those. PagerDuty/Opsgenie themselves can relay onward to SMS/voice per their own configuration, but this app has no native SMS/voice delivery of its own. rustinion_groupcan't be renamed, and can only be deleted while empty — there is no rename capability anywhere in this app, and a group with any devices or outstanding enrollment tokens can't be deleted (the same database constraints that already protect it everywhere else).template_keycan be changed freely in place at any time — it's the one real update path a group has — but there is no way to remove a template's effect once applied, only to apply a different one.rustinion_sloandrustinion_silencehave no in-place update at all — neither's underlying API has an update endpoint (both are create/read/delete-or-cancel only), matching their own admin UIs, which have never had an edit path for an existing SLO or silence either.rustinion_user'semail/group_idare fixed at invite time — there is no rename or move-to-a-different-group capability anywhere in this app; onlyaccess_level(admin/read_only) can be changed in place. Creating one sends the same one-time password-set email the admin UI's own invite does — silently skipped if the server has no SMTP configured, never a failure. Destroying it revokes the user's sessions if it was their last remaining access anywhere, matching the admin UI's own removal behavior.- One data source:
rustinion_process_watch_rules, looking up a group's process-watch rules (including ones the admin UI's own "Suggested rules" feature created) without managing them. Every other resource this API exposes is also writable — there had been no need for a read-only companion at all until this one. - One API key manages an entire tenant — there's no per-resource-
type scoping narrower than the existing
read_only/read_writesplit (0067_api_key_scope.sql), so aread_writekey configured for this provider can create/update/delete any resource type this provider manages across every group in its tenant, not just the ones a given Terraform configuration happens to touch. - Notification channel
config, synthetic check stepheaders/body, and integrationconfigare not markedSensitive— a Slack webhook URL, PagerDuty routing key, or anaws_cloudwatchintegration's access key insideconfigends up in Terraform state in plain text (markedSensitiveonly on the notification channel's ownconfigattribute, which redacts it from CLI output, but state files themselves are never encrypted by Terraform itself) — the same "state is sensitive" caveat that applies to any Terraform-managed secret, not specific to this provider, but worth calling out since every channel type but email, and one integration kind (aws_cloudwatch), holds one. rustinion_status_pageis a singleton — there is norustinion_group-style "which one" identifier, since the API itself has exactly one status page per tenant. Applying this resource against a tenant that already has a status page (created via the admin UI, say) overwrites it unconditionally on the nextapply, matching the API's own upsertPUTsemantics rather than failing with a conflict — there is no "adopt the existing page without changing it" import-only path.- No plan-time validation cross-checks between fields beyond what
each attribute's own
Validatorscover (e.g. anhttp_multistepcheck with a non-"direct"relay_modeis caught server-side, with a real Terraform error surfaced from the API's own 400 response, not rejected earlier at plan time by this provider itself).
See the main repo's docs/roadmap.md for the broader Datadog/Zabbix
competitive-gap context this provider was built to close.