api

package
v0.2.0-beta.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 19 Imported by: 0

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:

  1. Add the slice here, populated from the generated constants.
  2. 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.
  3. 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

View Source
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

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.

View Source
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.

View Source
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.

MatchRuleTypes lists every wire-format notification-policy match-rule kind. Used by the notification_policy resource's `match_rule[*].type` validator.

Functions

func AddAPIError

func AddAPIError(diagnostics *diag.Diagnostics, op string, err error, identityAttr path.Path)

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

func AlertChannelPath(id string) string

AlertChannelPath returns /api/v1/alert-channels/{id}.

func Create

func Create[T any](ctx context.Context, c *Client, path string, body any) (*T, error)

func CreateList

func CreateList[T any](ctx context.Context, c *Client, path string, body any) ([]T, error)

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 CreateRaw

func CreateRaw[T any](ctx context.Context, c *Client, path string, body any) (*T, []byte, error)

CreateRaw mirrors Create but also returns the raw response body. See GetRaw.

func Delete

func Delete(ctx context.Context, c *Client, path string) error

func DeleteWithBody

func DeleteWithBody(ctx context.Context, c *Client, path string, body any) error

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

func EnvironmentPath(slug string) string

EnvironmentPath returns /api/v1/environments/{slug}.

func Get

func Get[T any](ctx context.Context, c *Client, path string) (*T, error)

func GetRaw

func GetRaw[T any](ctx context.Context, c *Client, path string) (*T, []byte, error)

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

func IsNotFound(err error) bool

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 List

func List[T any](ctx context.Context, c *Client, basePath string) ([]T, error)

func MonitorPath

func MonitorPath(id string) string

MonitorPath returns /api/v1/monitors/{id}.

func MonitorTagsPath

func MonitorTagsPath(monitorID string) string

MonitorTagsPath returns /api/v1/monitors/{id}/tags.

func NotificationPolicyPath

func NotificationPolicyPath(id string) string

NotificationPolicyPath returns /api/v1/notification-policies/{id}.

func Patch

func Patch[T any](ctx context.Context, c *Client, path string, body any) (*T, error)

func PathEscape

func PathEscape(s string) string

func ResourceGroupMemberPath

func ResourceGroupMemberPath(groupID, memberID string) string

ResourceGroupMemberPath returns /api/v1/resource-groups/{groupId}/members/{memberId}.

func ResourceGroupMembersPath

func ResourceGroupMembersPath(groupID string) string

ResourceGroupMembersPath returns /api/v1/resource-groups/{id}/members.

func ResourceGroupPath

func ResourceGroupPath(id string) string

ResourceGroupPath returns /api/v1/resource-groups/{id}.

func SecretPath

func SecretPath(key string) string

SecretPath returns /api/v1/secrets/{key}.

func ServiceSubscriptionAlertSensitivityPath

func ServiceSubscriptionAlertSensitivityPath(id string) string

ServiceSubscriptionAlertSensitivityPath returns /api/v1/service-subscriptions/{id}/alert-sensitivity.

func ServiceSubscriptionPath

func ServiceSubscriptionPath(idOrSlug string) string

ServiceSubscriptionPath returns /api/v1/service-subscriptions/{idOrSlug}. Callers that pass user-supplied input should pre-escape the segment.

func StatusPageComponentPath

func StatusPageComponentPath(pageID, componentID string) string

StatusPageComponentPath returns /api/v1/status-pages/{pageId}/components/{componentId}.

func StatusPageComponentsPath

func StatusPageComponentsPath(pageID string) string

StatusPageComponentsPath returns /api/v1/status-pages/{id}/components.

func StatusPageDomainPath

func StatusPageDomainPath(pageID, domainID string) string

StatusPageDomainPath returns /api/v1/status-pages/{pageId}/domains/{domainId}.

func StatusPageDomainPrimaryPath

func StatusPageDomainPrimaryPath(pageID, domainID string) string

StatusPageDomainPrimaryPath returns /api/v1/status-pages/{pageId}/domains/{domainId}/primary.

func StatusPageDomainVerifyPath

func StatusPageDomainVerifyPath(pageID, domainID string) string

StatusPageDomainVerifyPath returns /api/v1/status-pages/{pageId}/domains/{domainId}/verify.

func StatusPageDomainsPath

func StatusPageDomainsPath(pageID string) string

StatusPageDomainsPath returns /api/v1/status-pages/{id}/domains.

func StatusPageGroupPath

func StatusPageGroupPath(pageID, groupID string) string

StatusPageGroupPath returns /api/v1/status-pages/{pageId}/groups/{groupId}.

func StatusPageGroupsPath

func StatusPageGroupsPath(pageID string) string

StatusPageGroupsPath returns /api/v1/status-pages/{id}/groups.

func StatusPagePath

func StatusPagePath(id string) string

StatusPagePath returns /api/v1/status-pages/{id}.

func TagPath

func TagPath(id string) string

TagPath returns /api/v1/tags/{id}.

func Update

func Update[T any](ctx context.Context, c *Client, path string, body any) (*T, error)

func UpdateRaw

func UpdateRaw[T any](ctx context.Context, c *Client, path string, body any) (*T, []byte, error)

UpdateRaw mirrors Update but also returns the raw response body. See GetRaw.

func ValidateDTO

func ValidateDTO(dto any, context string) error

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:

  1. Non-pointer fields must not be zero-valued (catches missing required fields that json.Unmarshal silently accepts).
  2. 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.

func WebhookPath

func WebhookPath(id string) string

WebhookPath returns /api/v1/webhooks/{id}.

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
}

func NewClient

func NewClient(baseURL, token, orgID, workspaceID, version string) *Client

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL