Documentation
¶
Overview ¶
Package webhook implements outbound webhooks: endpoint configuration stored in gk_webhook, a durable delivery log in gk_webhook_delivery, HMAC-signed HTTP delivery with bounded retries, and the event-source cursors product code uses to turn database changes into events exactly once.
Index ¶
- Constants
- Variables
- func AllowPrivateTargets() bool
- func CheckTargetHost(host string) error
- func NewHTTPClient(timeout time.Duration) *http.Client
- func Sign(secret string, body []byte) string
- func ValidateSecret(secret string) error
- type BlockedTargetError
- type Delivery
- type Dispatcher
- func (d *Dispatcher) Enqueue(ctx context.Context, tx *sql.Tx, hooks []*Webhook, event string, ...) (int, error)
- func (d *Dispatcher) ProcessDue(ctx context.Context) (int, error)
- func (d *Dispatcher) Redeliver(ctx context.Context, deliveryID int64) (*Delivery, error)
- func (d *Dispatcher) Test(ctx context.Context, webhookID int64) (*Delivery, error)
- type Envelope
- type Repository
- func (r *Repository) AdvanceCursor(ctx context.Context, tx *sql.Tx, source string, from, to int64) (bool, error)
- func (r *Repository) Create(ctx context.Context, w *Webhook, secret string, userID int) (*Webhook, error)
- func (r *Repository) Cursor(ctx context.Context, source string, start func(context.Context) (int64, error)) (int64, error)
- func (r *Repository) Delete(ctx context.Context, id int64) error
- func (r *Repository) Get(ctx context.Context, id int64) (*Webhook, error)
- func (r *Repository) GetDelivery(ctx context.Context, id int64) (*Delivery, error)
- func (r *Repository) HeaderValues(ctx context.Context, id int64) (map[string]string, error)
- func (r *Repository) List(ctx context.Context, active *bool) ([]*Webhook, error)
- func (r *Repository) ListDeliveries(ctx context.Context, webhookID int64, limit int) ([]*Delivery, error)
- func (r *Repository) PruneDeliveries(ctx context.Context, cutoff time.Time) (int, error)
- func (r *Repository) Update(ctx context.Context, w *Webhook, secret SecretChange, userID int) (*Webhook, error)
- type SecretChange
- type ValidationError
- type Webhook
Constants ¶
const ( StatusPending = "pending" StatusDelivering = "delivering" StatusDelivered = "delivered" StatusFailed = "failed" )
Delivery status values stored in gk_webhook_delivery.status.
const ( SignatureHeader = "X-Webhook-Signature" EventHeader = "X-Webhook-Event" DeliveryHeader = "X-Webhook-Delivery" UserAgent = "GoatFlow-Webhook/1.0" )
HTTP headers set on every outbound delivery. The signature header carries "sha256=<hex HMAC-SHA256 of the raw body keyed by the webhook secret>", the format GoatFlow's own inbound plugin webhooks verify.
const ( DefaultRetryCount = 3 MaxRetryCount = 10 DefaultTimeoutSeconds = 10 MaxTimeoutSeconds = 60 )
Limits and defaults for webhook configuration.
const AllowPrivateTargetsEnv = "GOATFLOW_WEBHOOK_ALLOW_PRIVATE_TARGETS"
AllowPrivateTargetsEnv names the environment variable that lets webhooks reach loopback, private, link-local and other internal addresses. It is an operator setting (not editable in the admin UI) because a webhook URL is otherwise a way for an admin account to make the server call internal services such as cloud metadata endpoints. Default: not allowed.
const MaxParallelWebhooks = 8
MaxParallelWebhooks bounds how many webhooks one ProcessDue call sends to at the same time. Deliveries to one webhook are sent one at a time, oldest first, so a slow or unreachable endpoint only delays its own deliveries.
const TestEvent = "webhook.test"
TestEvent is the event type of deliveries created by the test endpoint.
Variables ¶
var ErrDuplicateName = errors.New("a webhook with this name already exists")
ErrDuplicateName is returned when another webhook already uses the name.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned when a webhook or delivery does not exist.
Functions ¶
func AllowPrivateTargets ¶ added in v0.10.0
func AllowPrivateTargets() bool
AllowPrivateTargets reports whether AllowPrivateTargetsEnv is set to a true value ("true", "1", ...). It is read on every check, so tests and operators do not depend on process start order.
func CheckTargetHost ¶ added in v0.10.0
CheckTargetHost validates the host of an admin-supplied URL (webhook, identity-provider metadata) when it is saved. Only IP literals and the always-loopback name "localhost" (RFC 6761) can be judged without DNS; other names are checked on every connection, after resolution, by targetDialer.
func NewHTTPClient ¶ added in v0.10.0
NewHTTPClient returns a client with the delivery address guard (every resolved address checked and pinned, no redirects, no proxy) for other admin-supplied URLs the server fetches, such as identity-provider metadata and OIDC discovery documents. Requests give up after timeout.
func ValidateSecret ¶ added in v0.10.0
ValidateSecret checks a signing secret supplied by an administrator. An empty secret means "unsigned".
Types ¶
type BlockedTargetError ¶ added in v0.10.0
BlockedTargetError is returned when a webhook host is, or resolves to, an address deliveries may not reach.
func (*BlockedTargetError) Error ¶ added in v0.10.0
func (e *BlockedTargetError) Error() string
type Delivery ¶ added in v0.10.0
type Delivery struct {
ID int64 `json:"id"`
WebhookID int64 `json:"webhook_id"`
Event string `json:"event"`
Payload string `json:"payload,omitempty"`
Status string `json:"status"`
Success bool `json:"success"`
Attempts int `json:"attempts"`
StatusCode *int `json:"status_code"`
Response string `json:"response,omitempty"`
Error string `json:"error,omitempty"`
DurationMS *int `json:"duration_ms"`
NextAttemptAt *time.Time `json:"next_attempt_at,omitempty"`
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Delivery is one event sent (or to be sent) to one webhook.
type Dispatcher ¶ added in v0.10.0
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher fans events out to subscribed webhooks and delivers them.
func NewDispatcher ¶ added in v0.10.0
func NewDispatcher(repo *Repository) *Dispatcher
NewDispatcher returns a dispatcher using repo. Deliveries do not follow redirects and only reach internal addresses when AllowPrivateTargetsEnv allows it (see target.go).
func (*Dispatcher) Enqueue ¶ added in v0.10.0
func (d *Dispatcher) Enqueue(ctx context.Context, tx *sql.Tx, hooks []*Webhook, event string, occurredAt time.Time, data interface{}) (int, error)
Enqueue records a pending delivery of event for every active webhook in hooks that subscribes to it and returns how many were queued. Callers load hooks once (Repository.List) per batch of events. With a non-nil tx the rows are written inside that transaction.
func (*Dispatcher) ProcessDue ¶ added in v0.10.0
func (d *Dispatcher) ProcessDue(ctx context.Context) (int, error)
ProcessDue sends the due deliveries of every webhook, applying the retry policy of each, until none is due or ctx ends. Up to MaxParallelWebhooks webhooks are served concurrently. Deliveries still due when ctx ends stay pending for the next call; that is not an error. It returns the number of delivery attempts made.
type Envelope ¶ added in v0.10.0
type Envelope struct {
Event string `json:"event"`
OccurredAt time.Time `json:"occurred_at"`
Data interface{} `json:"data"`
}
Envelope is the JSON body POSTed to webhook endpoints.
type Repository ¶ added in v0.10.0
type Repository struct {
// contains filtered or unexported fields
}
Repository persists webhooks, deliveries and event-source cursors.
func NewRepository ¶ added in v0.10.0
func NewRepository(db *sql.DB) *Repository
NewRepository returns a repository on db.
func (*Repository) AdvanceCursor ¶ added in v0.10.0
func (r *Repository) AdvanceCursor(ctx context.Context, tx *sql.Tx, source string, from, to int64) (bool, error)
AdvanceCursor moves a cursor from "from" to "to" inside tx. It returns false when another worker already moved it, in which case the caller must roll back: the row lock taken here serialises concurrent dispatchers, so events are enqueued exactly once.
func (*Repository) Create ¶ added in v0.10.0
func (r *Repository) Create(ctx context.Context, w *Webhook, secret string, userID int) (*Webhook, error)
Create stores a new webhook (already normalised) and returns it.
func (*Repository) Cursor ¶ added in v0.10.0
func (r *Repository) Cursor(ctx context.Context, source string, start func(context.Context) (int64, error)) (int64, error)
Cursor returns the last processed id of an event source. When the source has no cursor yet it is created at start() so only later rows become events.
func (*Repository) Delete ¶ added in v0.10.0
func (r *Repository) Delete(ctx context.Context, id int64) error
Delete removes a webhook and (by cascade) its delivery log.
func (*Repository) Get ¶ added in v0.10.0
Get returns one webhook (without header values) or ErrNotFound.
func (*Repository) GetDelivery ¶ added in v0.10.0
GetDelivery returns one delivery including payload and response, or ErrNotFound.
func (*Repository) HeaderValues ¶ added in v0.10.0
HeaderValues returns the decrypted custom header values of a webhook, for updates that keep some stored values.
func (*Repository) List ¶ added in v0.10.0
List returns all webhooks ordered by name; active filters by state when set.
func (*Repository) ListDeliveries ¶ added in v0.10.0
func (r *Repository) ListDeliveries(ctx context.Context, webhookID int64, limit int) ([]*Delivery, error)
ListDeliveries returns the newest deliveries of a webhook without payload and response bodies (fetch a single delivery for those).
func (*Repository) PruneDeliveries ¶ added in v0.10.0
PruneDeliveries deletes delivered and failed deliveries created before cutoff and returns how many were deleted. Pending and in-flight deliveries are kept whatever their age.
func (*Repository) Update ¶ added in v0.10.0
func (r *Repository) Update(ctx context.Context, w *Webhook, secret SecretChange, userID int) (*Webhook, error)
Update stores a modified (already normalised) webhook. The custom headers are replaced when w.Headers is non-nil and kept otherwise.
type SecretChange ¶ added in v0.10.0
type SecretChange struct {
Set bool // replace the secret with Value ("" removes it)
Value string // new secret when Set
}
SecretChange tells Update what to do with the stored secret.
type ValidationError ¶ added in v0.10.0
type ValidationError struct {
Message string
}
ValidationError describes invalid webhook configuration.
func (*ValidationError) Error ¶ added in v0.10.0
func (e *ValidationError) Error() string
type Webhook ¶
type Webhook struct {
ID int64 `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Events []string `json:"events"`
// Headers holds custom header values. It is only filled when values are
// being saved or sent, never serialised.
Headers map[string]string `json:"-"`
// HeaderHints maps each custom header name to a masked hint of its value.
HeaderHints map[string]string `json:"header_hints"`
HasSecret bool `json:"has_secret"`
SecretHint string `json:"secret_hint,omitempty"`
RetryCount int `json:"retry_count"`
TimeoutSeconds int `json:"timeout_seconds"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
CreatedBy int `json:"created_by"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedBy int `json:"updated_by"`
}
Webhook is a configured outbound endpoint. The secret and the custom header values are write-only: they are stored encrypted and only masked hints are ever returned.
func (*Webhook) Normalize ¶ added in v0.10.0
Normalize trims input, applies defaults and validates the configuration. knownEvent reports whether an event name may be subscribed to. Headers are validated when set (nil means "not being changed").
func (*Webhook) Subscribes ¶ added in v0.10.0
Subscribes reports whether the webhook wants deliveries for event.