Skip to content

Add Mastodon alert provider - #1372

Open
xchose wants to merge 2 commits into
fluxcd:mainfrom
xchose:mastodon-provider
Open

Add Mastodon alert provider#1372
xchose wants to merge 2 commits into
fluxcd:mainfrom
xchose:mastodon-provider

Conversation

@xchose

@xchose xchose commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #447

Adds a mastodon Provider type for posting Flux events as statuses on a Mastodon account, via a plain HTTP POST to the /api/v1/statuses endpoint — no SDK dependency (the issue suggests mattn/go-mastodon, but like every other notifier this only needs the shared postMessage client).

Implementation

  • The address is the server root URL (e.g. https://mastodon.social); the /api/v1/statuses path is appended automatically (and preserved if already present).
  • Auth: OAuth access token with the write:statuses scope (Secret key token), sent as a bearer token.
  • Status text: severity emoji (💫/🚨, same convention as the Telegram provider), involved object kind/name.namespace, event message, and event metadata as key-value lines. Truncated to 500 characters — the default Mastodon server limit, which cannot be discovered without an extra API call, and exceeding it fails the post with a 422.
  • An Idempotency-Key header (SHA-256 over object UID, reason, timestamp and status text) guards against duplicate statuses when the retrying HTTP client loses a response.
  • An optional visibility query parameter on the address (public/unlisted/private) maps to the payload's visibility field and is stripped from the request URL — the Provider API has no dedicated field for it, and alert floods on the public timeline are likely unwanted.
  • Proxy and TLS configuration from the Provider spec are respected.

Sample rendered payload:

{
  "status": "💫 gitrepository/podinfo.flux-system\nstored artifact for commit 'master@sha1:eec06d1...'\n\nrevision: master@sha1:eec06d1...",
  "visibility": "unlisted"
}

Testing

  • internal/notifier/mastodon_test.go: asserts endpoint path, bearer auth header, Idempotency-Key presence, payload shape, severity emoji, visibility query-param mapping (including URL stripping), 500-char truncation, path preservation, and constructor validation against an httptest server.
  • Full envtest suite passes; make generate manifests tidy fmt vet leave a clean tree.

Manual e2e testing

kind cluster, controller image + CRD from this PR, real mastodon.social account. Secret: address: https://mastodon.social?visibility=unlisted, token with write:statuses. Info/error/recovery events from a podinfo GitRepository → 3× HTTP 2xx, statuses posted as unlisted:

mastodon-info mastodon-error

🤖 Generated with Claude Code

Full MD with testing:
e2e-comment.md

Add the mastodon Provider type for posting Flux events as statuses on
a Mastodon account. Events are published with a plain HTTP POST to the
/api/v1/statuses endpoint of the server given in the address, using an
OAuth access token with the write:statuses scope as a bearer token.

The status text carries a severity emoji, the involved object, the
event message and the event metadata as key-value lines in sorted key
order, truncated to the 500-character default server limit. An
Idempotency-Key header derived from the event guards against duplicate
statuses when the HTTP client retries a request whose response was
lost. An optional visibility query parameter on the address maps to the
status visibility field, since the Provider API has no dedicated field
for it.

Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Chose Carreras <xchose@gmail.com>
@xchose
xchose force-pushed the mastodon-provider branch from a2c1576 to f8ab539 Compare August 31, 2026 06:03
@xchose

xchose commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main, CI has never run on this PR: could a maintainer approve the run?

@xchose

xchose commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

CI is green now — thanks for approving the run.

The branch is rebased on main and still merges cleanly. Locally make test passes with a clean tree after codegen, and I verified the provider end-to-end against a real mastodon.social account.

Could a maintainer take a look when there's time?

@matheuscscp

Copy link
Copy Markdown
Member

It feels so strange to send a Flux alert to a social network 🤔 🤔 🤔

@stefanprodan

Copy link
Copy Markdown
Member

It feels so strange to send a Flux alert to a social network

Mastodon can be used like Slack, but self-hosted and private.

Comment on lines +1438 to +1439
Mastodon server limit) are truncated. An `Idempotency-Key` header derived from
the event is sent to prevent duplicate statuses on retried requests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we really need this since we already do this on our side, or if it would make sense to reuse the same key:

// eventKeyFunc generates a unique key for an event based on the provided HTTP
// request, which can be used to deduplicate events. The key is calculated by
// concatenating specific event attributes and hashing them using SHA-256.
// The key is then returned as a hex-encoded string.
//
// The event attributes are prefixed with an identifier to avoid collisions
// between different event attributes.
func eventKeyFunc(r *http.Request) (string, error) {
event := r.Context().Value(eventContextKey{}).(*eventv1.Event)
comps := []string{
"event",
"name=" + event.InvolvedObject.Name,
"namespace=" + event.InvolvedObject.Namespace,
"kind=" + event.InvolvedObject.Kind,
"message=" + event.Message,
}
objectGroup := event.InvolvedObject.GetObjectKind().GroupVersionKind().Group
originRevisionKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaOriginRevisionKey)
originRevision, ok := event.Metadata[originRevisionKey]
if ok {
comps = append(comps, "originRevision="+originRevision)
}
revisionKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaRevisionKey)
revision, ok := event.Metadata[revisionKey]
if ok {
comps = append(comps, "revision="+revision)
}
tokenKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaTokenKey)
token, ok := event.Metadata[tokenKey]
if ok {
comps = append(comps, "token="+token)
}
key := strings.Join(comps, "/")
digest := sha256.Sum256([]byte(key))
return fmt.Sprintf("%x", digest), nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we could do retries it feels like the key can be useful. In this case I wonder if reusing the same key we already have would make sense.

Comment on lines +74 to +79
// The visibility is carried as a query parameter of the address
// because the Provider API has no dedicated field for it.
q := u.Query()
visibility := q.Get("visibility")
q.Del("visibility")
u.RawQuery = q.Encode()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could someone mistakenly understand that the mastodon server accepts this query parameter? This solution feels a bit hacky... I can see other providers that don't exactly use a URL also benefiting from more inputs... Can we choose a sensible default here and only expose this input later, after putting more thought into how we can do this properly for all providers?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add mastodon as a notification channel

3 participants