Documentation
¶
Overview ¶
Package api — diagnostics helpers for mapping API errors to Terraform diagnostics.
The DevHelm API surfaces a uniform `{status, message, timestamp}` envelope for every non-2xx response. The TF provider's job is to translate those generic errors into per-attribute diagnostics so practitioners see the failing field highlighted in `terraform plan`/`apply` output instead of a summary message floating at the top of the run.
The helpers in this file centralize that translation. Resources call AddAPIError (or one of the operation-specific wrappers) instead of resp.Diagnostics.AddError directly, passing the attribute path (when known) that the error most plausibly originates from. The helper picks the most specific diagnostic shape available:
- 404 from a Read after import → AddAttributeError on path.Root("id")
- 409 ("already exists") on Create → AddAttributeError on the name field
- 400 with a "field <X>" hint → AddAttributeError on that path
- everything else → AddError with the operation context
Centralizing the mapping keeps the resource files free of repetitive switch-on-error-type boilerplate and ensures we apply the same rules everywhere.
Package api — derived enum lists.
The TF schema validators (stringvalidator.OneOf, …) need string slices to surface the legal value set in `terraform validate` output. The generated types declare each enum constant individually, so we re-export them as flat slices here. Codegen stays the source of truth — a new spec value will appear as a new constant in the generated package, and the test `TestEnumSliceCoverage` (in `enums_coverage_test.go`) verifies these slices stay exhaustive by reflecting over the generated package.
Adding a new slice for a generated enum type:
- Add the slice here, populated from the generated constants.
- Wire it into the relevant `Schema()` via `stringvalidator.OneOf(api.<X>...)` instead of literal string lists — this both eliminates DRY drift and keeps `TestEnumSliceCoverage` exhaustiveness in one place.
- Add the slice to the `enumSliceCoverage` table in `enums_coverage_test.go` so future spec additions force-fail until the slice is updated.
Index ¶
- Constants
- Variables
- func AddAPIError(diagnostics *diag.Diagnostics, op string, err error, identityAttr path.Path)
- func AddNotFoundError(diagnostics *diag.Diagnostics, resourceLabel, id string)
- func AlertChannelPath(id string) string
- func Create[T any](ctx context.Context, c *Client, path string, body any) (*T, error)
- func CreateList[T any](ctx context.Context, c *Client, path string, body any) ([]T, error)
- func CreateRaw[T any](ctx context.Context, c *Client, path string, body any) (*T, []byte, error)
- func Delete(ctx context.Context, c *Client, path string) error
- func DeleteWithBody(ctx context.Context, c *Client, path string, body any) error
- func EnvironmentPath(slug string) string
- func Get[T any](ctx context.Context, c *Client, path string) (*T, error)
- func GetRaw[T any](ctx context.Context, c *Client, path string) (*T, []byte, error)
- func IsNotFound(err error) bool
- func List[T any](ctx context.Context, c *Client, basePath string) ([]T, error)
- func MonitorPath(id string) string
- func MonitorTagsPath(monitorID string) string
- func NotificationPolicyPath(id string) string
- func Patch[T any](ctx context.Context, c *Client, path string, body any) (*T, error)
- func PathEscape(s string) string
- func ResourceGroupMemberPath(groupID, memberID string) string
- func ResourceGroupMembersPath(groupID string) string
- func ResourceGroupPath(id string) string
- func SecretPath(key string) string
- func ServiceSubscriptionAlertSensitivityPath(id string) string
- func ServiceSubscriptionPath(idOrSlug string) string
- func StatusPageComponentPath(pageID, componentID string) string
- func StatusPageComponentsPath(pageID string) string
- func StatusPageDomainPath(pageID, domainID string) string
- func StatusPageDomainPrimaryPath(pageID, domainID string) string
- func StatusPageDomainVerifyPath(pageID, domainID string) string
- func StatusPageDomainsPath(pageID string) string
- func StatusPageGroupPath(pageID, groupID string) string
- func StatusPageGroupsPath(pageID string) string
- func StatusPagePath(id string) string
- func TagPath(id string) string
- func Update[T any](ctx context.Context, c *Client, path string, body any) (*T, error)
- func UpdateRaw[T any](ctx context.Context, c *Client, path string, body any) (*T, []byte, error)
- func ValidateDTO(dto any, context string) error
- func WebhookPath(id string) string
- type Client
- type DevhelmAPIError
- type DevhelmTransportError
- type RequestBody
- type SingleValueResponse
- type TableResponse
Constants ¶
const ( // Top-level collections. PathAlertChannels = "/api/v1/alert-channels" PathEnvironments = "/api/v1/environments" PathMonitors = "/api/v1/monitors" PathNotificationPolicies = "/api/v1/notification-policies" PathResourceGroups = "/api/v1/resource-groups" PathSecrets = "/api/v1/secrets" PathServiceSubscriptions = "/api/v1/service-subscriptions" PathStatusPages = "/api/v1/status-pages" PathTags = "/api/v1/tags" PathWebhooks = "/api/v1/webhooks" )
Variables ¶
var AlertChannelTypes = []string{ string(generated.EmailChannelConfigChannelTypeEmail), string(generated.WebhookChannelConfigChannelTypeWebhook), string(generated.SlackChannelConfigChannelTypeSlack), string(generated.PagerDutyChannelConfigChannelTypePagerduty), string(generated.OpsGenieChannelConfigChannelTypeOpsgenie), string(generated.TeamsChannelConfigChannelTypeTeams), string(generated.DiscordChannelConfigChannelTypeDiscord), }
AlertChannelTypes lists every wire-format alert channel kind. Used by the alert_channel resource's `channel_type` validator (and by anything else that needs to discriminate channels by wire type).
Sourced from each alert-channel SUBTYPE's discriminator-tag constant (e.g. `EmailChannelConfigChannelTypeEmail`) rather than from the parent `AlertChannelDto.channelType` response enum, for the same reason as `AssertionTypes` above.
var AlertSensitivities = []string{
"ALL",
"INCIDENTS_ONLY",
"MAJOR_ONLY",
}
AlertSensitivities lists every wire-format alert-sensitivity value for the dependency / service-subscription resources.
`ServiceSubscriptionDto.alertSensitivity` is response-shaped, so under the spec-level Postel's-Law relaxation it has no typed alias. The request-side schema (`UpdateAlertSensitivityRequest.alertSensitivity`) uses an OpenAPI `pattern` instead of `enum`, so oapi-codegen also emits it as `string`. We therefore enumerate the allowed wire values here, and `TestEnumSliceCoverage` cross-checks them against the authoritative spec on each typegen run.
var AssertionTypes = []string{ string(generated.BodyContainsAssertionTypeBodyContains), string(generated.DnsExpectedCnameAssertionTypeDnsExpectedCname), string(generated.DnsExpectedIpsAssertionTypeDnsExpectedIps), string(generated.DnsMaxAnswersAssertionTypeDnsMaxAnswers), string(generated.DnsMinAnswersAssertionTypeDnsMinAnswers), string(generated.DnsRecordContainsAssertionTypeDnsRecordContains), string(generated.DnsRecordEqualsAssertionTypeDnsRecordEquals), string(generated.DnsResolvesAssertionTypeDnsResolves), string(generated.DnsResponseTimeAssertionTypeDnsResponseTime), string(generated.DnsResponseTimeWarnAssertionTypeDnsResponseTimeWarn), string(generated.DnsTtlHighAssertionTypeDnsTtlHigh), string(generated.DnsTtlLowAssertionTypeDnsTtlLow), string(generated.DnsTxtContainsAssertionTypeDnsTxtContains), string(generated.HeaderValueAssertionTypeHeaderValue), string(generated.HeartbeatIntervalDriftAssertionTypeHeartbeatIntervalDrift), string(generated.HeartbeatMaxIntervalAssertionTypeHeartbeatMaxInterval), string(generated.HeartbeatPayloadContainsAssertionTypeHeartbeatPayloadContains), string(generated.HeartbeatReceivedAssertionTypeHeartbeatReceived), string(generated.IcmpPacketLossAssertionTypeIcmpPacketLoss), string(generated.IcmpReachableAssertionTypeIcmpReachable), string(generated.IcmpResponseTimeAssertionTypeIcmpResponseTime), string(generated.IcmpResponseTimeWarnAssertionTypeIcmpResponseTimeWarn), string(generated.JsonPathAssertionTypeJsonPath), string(generated.McpConnectsAssertionTypeMcpConnects), string(generated.McpHasCapabilityAssertionTypeMcpHasCapability), string(generated.McpMinToolsAssertionTypeMcpMinTools), string(generated.McpProtocolVersionAssertionTypeMcpProtocolVersion), string(generated.McpResponseTimeAssertionTypeMcpResponseTime), string(generated.McpResponseTimeWarnAssertionTypeMcpResponseTimeWarn), string(generated.McpToolAvailableAssertionTypeMcpToolAvailable), string(generated.McpToolCountChangedAssertionTypeMcpToolCountChanged), string(generated.RedirectCountAssertionTypeRedirectCount), string(generated.RedirectTargetAssertionTypeRedirectTarget), string(generated.RegexBodyAssertionTypeRegexBody), string(generated.ResponseSizeAssertionTypeResponseSize), string(generated.ResponseTimeAssertionTypeResponseTime), string(generated.ResponseTimeWarnAssertionTypeResponseTimeWarn), string(generated.SslExpiryAssertionTypeSslExpiry), string(generated.StatusCodeAssertionTypeStatusCode), string(generated.TcpConnectsAssertionTypeTcpConnects), string(generated.TcpResponseTimeAssertionTypeTcpResponseTime), string(generated.TcpResponseTimeWarnAssertionTypeTcpResponseTimeWarn), }
AssertionTypes lists every wire-format assertion type. Used by the monitor resource's `assertions[*].type` validator.
Sourced from each assertion SUBTYPE's discriminator-tag constant (e.g. `BodyContainsAssertionTypeBodyContains BodyContainsAssertionType = "body_contains"`) rather than from the parent `MonitorAssertionDto.assertionType` response enum. Under the spec-level Postel's-Law relaxation (`mini/runbooks/api-contract.md` § 3), response-DTO multi-value enums are dropped and the parent typed alias no longer exists. Subtype discriminator tags are single-value enums and survive — they're the canonical source for plan-time validation here. Constant names are pinned by `compatibility.always-prefix-enum-values: true` in `scripts/oapi-codegen.yaml` so unrelated enum churn cannot rename them.
var MatchRuleTypes = []string{ string(generated.MatchRuleTypeComponentNameIn), string(generated.MatchRuleTypeIncidentStatus), string(generated.MatchRuleTypeMonitorIdIn), string(generated.MatchRuleTypeMonitorTagIn), string(generated.MatchRuleTypeMonitorTypeIn), string(generated.MatchRuleTypeRegionIn), string(generated.MatchRuleTypeResourceGroupIdIn), string(generated.MatchRuleTypeServiceIdIn), string(generated.MatchRuleTypeSeverityGte), }
MatchRuleTypes lists every wire-format notification-policy match-rule kind. Used by the notification_policy resource's `match_rule[*].type` validator.
Functions ¶
func AddAPIError ¶
AddAPIError translates a generic API error into the most specific diagnostic shape available. `op` is a short imperative verb describing what the resource was attempting (e.g. "create monitor"). `identityAttr` is the path to the attribute that uniquely names the resource within the workspace (typically path.Root("name") or path.Root("slug")); pass path.Empty() when no such anchor applies.
The function is a no-op when err is nil so callers can wrap the entire API call site without needing a separate guard.
func AddNotFoundError ¶
func AddNotFoundError(diagnostics *diag.Diagnostics, resourceLabel, id string)
AddNotFoundError emits the import-time "resource not found" diagnostic in a uniform shape across all resources. Use during ImportState when a list lookup returns zero results for the requested ID.
func AlertChannelPath ¶
AlertChannelPath returns /api/v1/alert-channels/{id}.
func CreateList ¶
CreateList POSTs to an endpoint that returns a TableResponse[T] (e.g. the tag-management sub-resources on monitors, which return the full collection after the mutation rather than a single entity). Use Create when the endpoint returns SingleValueResponse[T].
func DeleteWithBody ¶
DeleteWithBody issues a DELETE with a JSON request body. Used for endpoints that accept a body (e.g. DELETE /monitors/{id}/tags with a list of tag IDs).
func EnvironmentPath ¶
EnvironmentPath returns /api/v1/environments/{slug}.
func GetRaw ¶
GetRaw is the escape-hatch variant of Get that also returns the raw response body so callers can extract polymorphic / discriminated-union fields whose generated Go type loses information during a typed unmarshal (e.g. monitor `auth`, where the spec collapsed the oneOf into a base `MonitorAuthConfig{Type string}` and only the `type` discriminator survives). Use the typed result for everything else; only reach into the raw body for the specific field whose round-trip you need to preserve.
func IsNotFound ¶
IsNotFound reports whether err is a *DevhelmAPIError with HTTP 404. Used by resources during Read to translate "deleted out-of-band" into the disappear-from-state path Terraform expects.
func MonitorTagsPath ¶
MonitorTagsPath returns /api/v1/monitors/{id}/tags.
func NotificationPolicyPath ¶
NotificationPolicyPath returns /api/v1/notification-policies/{id}.
func PathEscape ¶
func ResourceGroupMemberPath ¶
ResourceGroupMemberPath returns /api/v1/resource-groups/{groupId}/members/{memberId}.
func ResourceGroupMembersPath ¶
ResourceGroupMembersPath returns /api/v1/resource-groups/{id}/members.
func ResourceGroupPath ¶
ResourceGroupPath returns /api/v1/resource-groups/{id}.
func ServiceSubscriptionAlertSensitivityPath ¶
ServiceSubscriptionAlertSensitivityPath returns /api/v1/service-subscriptions/{id}/alert-sensitivity.
func ServiceSubscriptionPath ¶
ServiceSubscriptionPath returns /api/v1/service-subscriptions/{idOrSlug}. Callers that pass user-supplied input should pre-escape the segment.
func StatusPageComponentPath ¶
StatusPageComponentPath returns /api/v1/status-pages/{pageId}/components/{componentId}.
func StatusPageComponentsPath ¶
StatusPageComponentsPath returns /api/v1/status-pages/{id}/components.
func StatusPageDomainPath ¶
StatusPageDomainPath returns /api/v1/status-pages/{pageId}/domains/{domainId}.
func StatusPageDomainPrimaryPath ¶
StatusPageDomainPrimaryPath returns /api/v1/status-pages/{pageId}/domains/{domainId}/primary.
func StatusPageDomainVerifyPath ¶
StatusPageDomainVerifyPath returns /api/v1/status-pages/{pageId}/domains/{domainId}/verify.
func StatusPageDomainsPath ¶
StatusPageDomainsPath returns /api/v1/status-pages/{id}/domains.
func StatusPageGroupPath ¶
StatusPageGroupPath returns /api/v1/status-pages/{pageId}/groups/{groupId}.
func StatusPageGroupsPath ¶
StatusPageGroupsPath returns /api/v1/status-pages/{id}/groups.
func StatusPagePath ¶
StatusPagePath returns /api/v1/status-pages/{id}.
func ValidateDTO ¶
ValidateDTO performs structural validation on a DTO returned by the API.
It leverages the fact that oapi-codegen encodes the OpenAPI required/optional distinction directly in Go's type system:
- Required fields are value types (string, UUID, enum, time.Time)
- Optional fields are pointer types (*string, *bool, *int32)
The validator walks the struct and enforces two invariants:
- Non-pointer fields must not be zero-valued (catches missing required fields that json.Unmarshal silently accepts).
- Fields whose type implements Valid() bool must return true (catches known-bad values for the enums that survive spec-level relaxation — primarily single-value discriminator tags).
Note on Postel's-Law tolerance (see `mini/runbooks/api-contract.md` § 3): multi-value enums on response-shaped DTOs are dropped from the spec before codegen, so those fields are emitted as plain `string` and trivially skip the `Valid()` check. That is the desired behaviour — adding a new wire-format value to e.g. `MonitorDto.type` must NOT break existing provider versions reading existing resources. The `Valid()` branch only fires for enums that intentionally remain strict (request DTOs and discriminator subtype tags).
This is the Go equivalent of Zod safeParse (SDK-JS) and Pydantic model_validate (SDK-Python) — runtime response validation driven by the spec, with zero hand-written per-DTO code.
Types ¶
type Client ¶
type Client struct {
BaseURL string
Token string
OrgID string
WorkspaceID string
HTTPClient *http.Client
UserAgent string
// SurfaceHeaders is the X-DevHelm-Surface* set sent on every request so
// the API can attribute usage to the Terraform provider for adoption /
// version-distribution telemetry. Computed once in NewClient — empty
// when DEVHELM_TELEMETRY=0 so the user opt-out lives in one env var
// rather than being threaded through every resource. Wire contract:
// https://devhelm.io/telemetry.
SurfaceHeaders map[string]string
}
type DevhelmAPIError ¶
type DevhelmAPIError struct {
StatusCode int
Code string
Message string
RequestID string
Body string
}
DevhelmAPIError represents a non-2xx response from the DevHelm API. It is the error class every TF resource wraps via `api.AddAPIError` (or branches on via `api.IsNotFound`).
- StatusCode is the HTTP status line (always non-zero for this error type).
- Code mirrors `ErrorResponse.code` from the API — a stable, machine- readable category like "NOT_FOUND" or "RATE_LIMITED". May be empty for legacy error bodies that do not include the field.
- Message is the human-readable text from `ErrorResponse.message`, or the `error` field, or — when neither is present — the raw response body.
- RequestID is the `X-Request-Id` response header. Always include it in support tickets; when blank, the response did not carry the header (should not happen against the production API).
- Body is the raw response body, retained for debugging non-conforming replies.
func (*DevhelmAPIError) Error ¶
func (e *DevhelmAPIError) Error() string
type DevhelmTransportError ¶
type DevhelmTransportError struct {
// Op describes the failing transport step ("build request", "send
// request", "read response"). Stable enough to switch on in tests.
Op string
// URL is the resolved target URL, useful when diagnosing DNS or TLS
// issues against a specific endpoint.
URL string
// Err is the underlying transport-level error.
Err error
}
DevhelmTransportError represents a failure to complete an HTTP exchange: the request never reached the server, the server never returned a complete response, or a TLS/DNS/connection layer surfaced an error. Wraps the underlying error so callers can `errors.As` for the original cause.
func (*DevhelmTransportError) Error ¶
func (e *DevhelmTransportError) Error() string
func (*DevhelmTransportError) Unwrap ¶
func (e *DevhelmTransportError) Unwrap() error
type RequestBody ¶
type RequestBody = any
RequestBody is the P5-tracked boundary type for any request payload that `doRequest` will serialize via `json.Marshal`. Callers must pass a typed struct from `internal/generated` (or a properly tagged handwritten equivalent), never a raw `map[string]any`. The alias is here so a future audit can grep for `RequestBody` and find every site that crosses the json.Marshal boundary, even though Go's type system can't prevent the `map[string]any` case at compile time.
type SingleValueResponse ¶
type SingleValueResponse[T any] struct { Data T `json:"data"` }
SingleValueResponse is the single-value envelope used by most endpoints.
Note on the zero-value contract: when the upstream JSON is malformed or the server returns a 2xx without a `data` field (which the API contract should never produce, but is worth being explicit about), `Data` will be the zero value of `T` and `Get`/`Create`/`Update`/`Patch` will return a non-nil pointer to that zero value. Callers should NOT treat a non-nil return as "the resource exists" — they should rely on the per-resource semantic invariants instead (e.g. `dto.Id != uuid.Nil` for newly-created resources, or `IsNotFound(err)` for explicit 404 handling). Nil-checking `&resp.Data` itself is meaningless because the address of a value-typed struct field inside a stack-allocated struct is always non-nil.
type TableResponse ¶
type TableResponse[T any] struct { Data []T `json:"data"` HasNext bool `json:"hasNext"` HasPrev bool `json:"hasPrev,omitempty"` TotalElements *int64 `json:"totalElements,omitempty"` TotalPages *int32 `json:"totalPages,omitempty"` }
Table response wrapper used by list endpoints. Mirrors the API's full pagination envelope. Fields beyond Data/HasNext are unused by the provider today; with the lenient decoder they would be ignored regardless, but declaring them keeps the drift-warn signal honest (otherwise every list response would warn about hasPrev/totalElements/totalPages).