The Grafana target Config takes an auth block that selects an
authentication strategy: TokenAuth for a service account token or API key,
BasicAuth for a username and password. Every credential field accepts a
literal string or a resolvable, so a service account token can now be sourced
from a formae-managed secret and resolved live at apply time. Previously only
basic auth could be sourced that way, and a token could reach the plugin only
through the GRAFANA_AUTH environment variable, which forced a target wanting
secret-sourced credentials onto instance-superuser admin basic auth. When
auth is set, GRAFANA_AUTH is not consulted and an incomplete block is an
error rather than a silent fallback to the environment. Omitting auth keeps
the existing GRAFANA_AUTH behaviour unchanged.
The target Config declares per-field mutability, so auth and orgId can
change on an existing target. Target config fields are immutable unless
annotated, which meant any edit to a Grafana target was classified as a
replace. Grafana resources are not portable across targets, so the apply was
rejected outright with NonPortableResources, listing every folder, dashboard
and datasource bound to the target. Adopting the auth block on a live target
was therefore impossible without first destroying all of them. url is
deliberately still immutable: it is where the resources live, so two URLs are
two Grafana databases, and updating in place would leave recorded state for
objects that do not exist at the new address.
NotificationPolicy.receiver now accepts a resolvable to a contact point's
name (myContactPoint.res.name) as well as a literal string, and
ContactPoint exposes a res resolvable to support it. Grafana rejects a
policy tree naming a receiver that does not exist, so previously a first apply
could send the policy before its contact point existed and fail, and a destroy
could delete the contact point before the policy was reset, leaving the tree
inconsistent. Using the resolvable makes the dependency explicit, so formae
creates the contact point first and resets the policy first on teardown.
Receivers named inside routes are part of an opaque JSON string and still
carry no dependency edge.
The target Config takes an optional proxyUrl, scoped to that target
alone: no other target is affected. It accepts socks5://, socks5h:// or
http://; socks5 and socks5h are aliases in Go and behave identically,
always handing the destination hostname to the proxy rather than resolving
it locally. Setting it makes the target ignore the ambient HTTP_PROXY,
HTTPS_PROXY and NO_PROXY environment variables entirely. Leaving it
unset changes nothing: the target still dials through Go's default
transport, which continues to honour those variables exactly as before.
There is no https:// proxy yet, since a TLS connection to the proxy itself
generally needs a private CA or a client certificate this field cannot
express, and no support for proxy credentials yet either. A proxyUrl that
is malformed, names an unsupported scheme, carries credentials, or is
otherwise unusable is rejected as an error when the client is constructed,
rather than silently falling back to unproxied dialing.
Config fields username and password are replaced by the
auth block: write auth = new grafana.BasicAuth { username = …; password = … }.
The fields existed only in the 0.1.7-dev.0 prerelease, so no released
version is affected.An alert rule, notification policy or mute timing whose content is unchanged
now reconciles as a true 0-change instead of reporting an update on every
apply. AlertRule.data, AlertRule.labels, AlertRule.annotations,
NotificationPolicy.routes, NotificationPolicy.groupBy and
MuteTiming.timeIntervals each carry a serialized JSON document in a plain
String, and none declared a format, so formae compared them byte-for-byte:
the authored Pkl source text against the plugin's marshalled output. Those
two agree on content but not on key order, so every rule drifted forever and
an empty change set stopped being a meaningful signal. All six are now
declared as serialized JSON, so both sides are canonicalized before the
update diff — the same declaration a dashboard's configJson already
carries.
Two consequences worth knowing. Canonicalization equates two spellings of the
same document, not two different documents: members Grafana populates when
you omit them (queryType and relativeTimeRange inside data), members
stripped on read (a null-valued member of a timeIntervals interval) and an
authored "{}" against an absent field are differences in content and still
diff — declare those values explicitly. And because these fields are now
parsed during the diff, a malformed JSON value can surface as a
planning-time failure where it was previously discarded silently and the
resource was written without that member.
Updating a contact point no longer records its secret settings in cleartext.
Grafana stores the secret fields of a contact point encrypted (a Slack url,
a PagerDuty integrationKey, a webhook password) and returns them as
[REDACTED] on every read path. Update echoed back the settings it had just
sent instead of the server's view, so the plaintext secret was written into
recorded state on every update, and stayed permanently diverged from what a
read reported. Update now reports the state read back from Grafana, which is
what create already did.
NetworkFailure/ServiceTimeout rather than the InternalFailure
default, so the agent can tell "unreachable" apart from "deleted" and
eventually reap a permanently-gone target.read("dashboard.json")) is stored by
Grafana in its own compacted, key-reordered form. Previously every sync
reported the dashboard as changed and every apply generated a no-op update,
pure whitespace and key-ordering noise, even when nothing about the dashboard
had actually changed. The dashboard JSON is now compared by content, so an
unchanged dashboard stays quiet, and reformatting your source file
(reindenting or reordering keys) on its own no longer triggers an update.Contact point settings as resolvables (settingsMap): GRAFANA::Alerting::ContactPoint
gains a settingsMap field whose values accept formae resolvables, so a
setting can flow in from another resource (even one managed by a different
plugin) in a single apply. This is what lets a PagerDuty integration key wire
straight into a Grafana contact point:
settingsMap = new Mapping {
["integrationKey"] = pdIntegration.res.integrationKey
}Provide exactly one of settings (the JSON-string form) or settingsMap.
createOnly: fields the Grafana API will not
change in place (for example a contact point's name) are now annotated
createOnly, so formae replaces the resource instead of attempting an invalid
in-place update.GRAFANA::: the namespace prefix is now
uppercase to match the formae convention (namespace = "GRAFANA"). All ten
resource types are affected (folder, dashboard, alert_rule, contact_point,
datasource, message_template, mute_timing, notification_policy,
service_account, team). The schema generates these names automatically, so PKL
forma files that import the Grafana schema don't need any change. Forma files
that reference the resource types by string (e.g. in queries) should switch
from Grafana::… to GRAFANA::….Resolvable target URL: The Grafana target URL now accepts resolvable references, so you can wire it directly to another resource's output. For example, connect Grafana to a compose stack endpoint without workarounds:
config = new grafana.Config {
url = lgtmStack.res.endpoints.at("lgtm:3000")
}The Endpoints/EndpointKey pattern still works but is deprecated and will
be removed in a future release.
jsonData values no longer cause drift on
every sync. Grafana-populated defaults are now recognised as provider
defaults.folderUid can now use a
resolvable reference (folder.res.uid), ensuring the folder is created before
the resources that depend on it.