@marble-sh/backstage-plugin-scaffolder-backend-module-grafana 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/README.md +25 -13
- package/config.schema.json +1 -1
- package/dist/actions/createDashboard.cjs.js +42 -23
- package/dist/actions/createDashboard.cjs.js.map +1 -1
- package/dist/module.cjs.js +20 -0
- package/dist/module.cjs.js.map +1 -1
- package/package.json +11 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @marble-sh/backstage-plugin-scaffolder-backend-module-grafana
|
|
2
|
+
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c9c9f23: Documentation: a full entity-annotation reference (exact matching semantics,
|
|
8
|
+
how the annotations combine, visibility gating, and error behavior for
|
|
9
|
+
unknown instance names) in the frontend and common READMEs, and a new
|
|
10
|
+
"Creating the Grafana service account and token" walkthrough in the backend
|
|
11
|
+
README — UI steps, the Grafana Cloud `glsa_` vs `glc_` token distinction,
|
|
12
|
+
and a per-feature permission table (Viewer covers all read paths;
|
|
13
|
+
`datasources:query` caveat for panel graphs under Enterprise/Cloud data
|
|
14
|
+
source permissions; Editor / `fixed:dashboards:writer` for the scaffolder
|
|
15
|
+
module).
|
|
16
|
+
- 33ad02e: Hardening fixes from a cross-package audit:
|
|
17
|
+
|
|
18
|
+
- **Backend**: a read that fans out over all instances now skips (and logs)
|
|
19
|
+
an unreachable instance instead of failing the whole request; a named
|
|
20
|
+
instance still propagates its error. The panel routes accept
|
|
21
|
+
`refresh=true` (gated by `allowOnDemandRefresh`) to bypass the panel
|
|
22
|
+
cache, and the frontend's "Refresh panels" button uses it — previously
|
|
23
|
+
the button silently served cached data. Startup warns when
|
|
24
|
+
`store: database` is combined with no `schedule` (snapshots would never
|
|
25
|
+
expire).
|
|
26
|
+
- **Frontend**: `GrafanaApi.listPanels`/`getPanelData` are now optional, as
|
|
27
|
+
the changelog already promised — pre-existing custom implementations
|
|
28
|
+
compile again; the dashboards tab falls back to a Grafana link when they
|
|
29
|
+
are absent.
|
|
30
|
+
- **Node library**: the per-instance dashboard-model cache evicts expired
|
|
31
|
+
entries instead of growing forever; an empty `__panelId__` annotation is
|
|
32
|
+
no longer parsed as panel `0`; generated fallback refIds can no longer
|
|
33
|
+
collide with an explicitly declared refId (Grafana keys query results by
|
|
34
|
+
refId); failed _hidden_ queries no longer surface user-facing warnings.
|
|
35
|
+
- **Catalog module**: discovered dashboard entities now carry only
|
|
36
|
+
`grafana/dashboard-uid`, not a title-based `grafana/dashboard-selector` —
|
|
37
|
+
the selectors AND together, so a stale title (dashboard renamed in
|
|
38
|
+
Grafana between discovery runs) hid the dashboard its own uid still
|
|
39
|
+
matched.
|
|
40
|
+
- **Scaffolder module**: `grafana.scaffolder.allowedInstances` is validated
|
|
41
|
+
at startup (like the catalog module) instead of on every run; an
|
|
42
|
+
`overwrite` update reads the current dashboard first, carries its
|
|
43
|
+
`metadata.resourceVersion` into the `PUT`, and degrades to a plain create
|
|
44
|
+
when the dashboard does not exist yet, keeping template runs idempotent.
|
|
45
|
+
|
|
46
|
+
- 9664e66: Documentation consistency pass: the backend README no longer claims the
|
|
47
|
+
catalog module consumes the REST API (it uses the shared node client
|
|
48
|
+
server-side); the scaffolder README/config schema document startup
|
|
49
|
+
validation of `allowedInstances` and the idempotent
|
|
50
|
+
`overwrite` (resourceVersion carry, create fallback); fan-out
|
|
51
|
+
failure-skipping and panel-route `refresh` are documented; and the
|
|
52
|
+
grafana-node README credits all three consumers.
|
|
53
|
+
- Updated dependencies [33ad02e]
|
|
54
|
+
- Updated dependencies [9664e66]
|
|
55
|
+
- @marble-sh/backstage-plugin-grafana-node@1.2.1
|
|
56
|
+
|
|
57
|
+
## 1.0.2
|
|
58
|
+
|
|
59
|
+
### Patch Changes
|
|
60
|
+
|
|
61
|
+
- d5daa0c: Fixed: `release:publish` now actually invokes `scripts/release-publish.mjs`.
|
|
62
|
+
The previous fix added the script but left the release script running
|
|
63
|
+
`changeset publish`, so 1.0.1 was published with the same raw `workspace:^`
|
|
64
|
+
and `backstage:^` ranges as 1.0.0 and remains uninstallable outside this
|
|
65
|
+
monorepo. This release is the first one packed with Yarn (materialized
|
|
66
|
+
dependency ranges).
|
|
67
|
+
- Updated dependencies [d5daa0c]
|
|
68
|
+
- @marble-sh/backstage-plugin-grafana-node@1.0.2
|
|
69
|
+
|
|
70
|
+
## 1.0.1
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- 577eaca: Fixed: published manifests now carry real semver dependency ranges. Versions
|
|
75
|
+
0.2.0 and 1.0.0 were published via `changeset publish` (plain `npm publish`),
|
|
76
|
+
which skips Yarn's pack hooks and leaked the raw `workspace:^` and
|
|
77
|
+
`backstage:^` protocols into the registry manifests, making the packages
|
|
78
|
+
uninstallable outside this monorepo. Releases now publish `yarn pack` tarballs
|
|
79
|
+
through the npm CLI.
|
|
80
|
+
- Updated dependencies [577eaca]
|
|
81
|
+
- @marble-sh/backstage-plugin-grafana-node@1.0.1
|
|
82
|
+
|
|
83
|
+
## 1.0.0
|
|
84
|
+
|
|
85
|
+
### Major Changes
|
|
86
|
+
|
|
87
|
+
- b179a8f: Bumping to version 1.0.0, General Release!
|
|
88
|
+
|
|
89
|
+
### Patch Changes
|
|
90
|
+
|
|
91
|
+
- Updated dependencies [b179a8f]
|
|
92
|
+
- @marble-sh/backstage-plugin-grafana-node@1.0.0
|
|
93
|
+
|
|
94
|
+
## 0.2.0
|
|
95
|
+
|
|
96
|
+
### Minor Changes
|
|
97
|
+
|
|
98
|
+
- 72afd3d: Initial release of the Grafana plugin suite: a read-only, backend-centric
|
|
99
|
+
Grafana integration built on Backstage's new backend system, with a frontend
|
|
100
|
+
that supports both the legacy and the new frontend systems.
|
|
101
|
+
|
|
102
|
+
- `grafana-common` — shared entity annotations and data-transfer types.
|
|
103
|
+
- `grafana-node` — shared Grafana HTTP client (App Platform APIs, with folder
|
|
104
|
+
resolution), instance config reader + schema, and filters. Dashboard
|
|
105
|
+
selection supports comma-separated multi-value queries, and per-instance
|
|
106
|
+
flags can disable dashboards, alerts, or folder resolution.
|
|
107
|
+
- `grafana-backend` — read-only REST API with cache/database storage,
|
|
108
|
+
scheduled refresh, and `allowOnDemandRefresh` / `fetchOnDemand` flags to
|
|
109
|
+
make Grafana traffic fully deterministic. Discovery scoping and scaffolder
|
|
110
|
+
guard-rail options round out the configuration surface.
|
|
111
|
+
- `grafana` — entity dashboard/alert cards and content plus a standalone
|
|
112
|
+
instances page, for both frontend systems (new system via the `/alpha`
|
|
113
|
+
export).
|
|
114
|
+
- `catalog-backend-module-grafana` — discovers Grafana instances and dashboards
|
|
115
|
+
as catalog `Resource` entities with dependency relations.
|
|
116
|
+
- `scaffolder-backend-module-grafana` — a `grafana:dashboard:create` scaffolder
|
|
117
|
+
action.
|
|
118
|
+
|
|
119
|
+
### Patch Changes
|
|
120
|
+
|
|
121
|
+
- Updated dependencies [72afd3d]
|
|
122
|
+
- @marble-sh/backstage-plugin-grafana-node@0.2.0
|
package/README.md
CHANGED
|
@@ -16,18 +16,22 @@ scaffolder.
|
|
|
16
16
|
Creates (or, with `overwrite`, updates) a dashboard in a configured Grafana
|
|
17
17
|
instance via the App Platform `dashboard.grafana.app/v1` API.
|
|
18
18
|
|
|
19
|
-
| Input | Type | Required | Description
|
|
20
|
-
| -------------- | ---------- | -------- |
|
|
21
|
-
| `title` | `string` | yes | The dashboard title.
|
|
22
|
-
| `instanceName` | `string` | no | Which configured instance to target. Optional when only one is configured.
|
|
23
|
-
| `uid` | `string` | no | Dashboard uid (`metadata.name`). Grafana generates one when omitted.
|
|
24
|
-
| `folderUid` | `string` | no | The uid of the folder to create the dashboard in.
|
|
25
|
-
| `tags` | `string[]` | no | Dashboard tags.
|
|
26
|
-
| `dashboard` | `object` | no | Extra dashboard spec fields (panels, templating, …) merged into the spec.
|
|
27
|
-
| `overwrite` | `boolean` | no | Update the dashboard
|
|
19
|
+
| Input | Type | Required | Description |
|
|
20
|
+
| -------------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `title` | `string` | yes | The dashboard title. |
|
|
22
|
+
| `instanceName` | `string` | no | Which configured instance to target. Optional when only one is configured. |
|
|
23
|
+
| `uid` | `string` | no | Dashboard uid (`metadata.name`). Grafana generates one when omitted. |
|
|
24
|
+
| `folderUid` | `string` | no | The uid of the folder to create the dashboard in. |
|
|
25
|
+
| `tags` | `string[]` | no | Dashboard tags. |
|
|
26
|
+
| `dashboard` | `object` | no | Extra dashboard spec fields (panels, templating, …) merged into the spec. |
|
|
27
|
+
| `overwrite` | `boolean` | no | Update the dashboard with the given `uid` (requires `uid`). Idempotent: when no such dashboard exists yet, it is created instead. |
|
|
28
28
|
|
|
29
29
|
Outputs: `uid`, `url`, and `instanceName`.
|
|
30
30
|
|
|
31
|
+
An `overwrite` run first reads the current dashboard and carries its
|
|
32
|
+
`metadata.resourceVersion` into the update, so it plays by the App Platform
|
|
33
|
+
API's optimistic-concurrency rules.
|
|
34
|
+
|
|
31
35
|
Example template step:
|
|
32
36
|
|
|
33
37
|
```yaml
|
|
@@ -62,8 +66,16 @@ backend.add(
|
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
The action reads its connection details from `grafana.instances` (see the
|
|
65
|
-
[backend plugin](../grafana-backend/README.md) for that configuration).
|
|
66
|
-
|
|
69
|
+
[backend plugin](../grafana-backend/README.md) for that configuration).
|
|
70
|
+
|
|
71
|
+
Unlike the read-only plugins, this module **writes** to Grafana, so a Viewer
|
|
72
|
+
token is not enough: the target instance's service account needs
|
|
73
|
+
`dashboards:read` and `dashboards:create` — plus `dashboards:write` when
|
|
74
|
+
templates use `overwrite: true` — in the folders your templates target. The
|
|
75
|
+
simplest setup is the **Editor** basic role; with RBAC (Grafana
|
|
76
|
+
Enterprise/Cloud) use `fixed:dashboards:writer` scoped to those folders. See
|
|
77
|
+
[Creating the Grafana service account and token](../grafana-backend/README.md#creating-the-grafana-service-account-and-token)
|
|
78
|
+
for the walkthrough.
|
|
67
79
|
|
|
68
80
|
## Guard rails (`grafana.scaffolder`)
|
|
69
81
|
|
|
@@ -84,8 +96,8 @@ grafana:
|
|
|
84
96
|
is omitted) only considers the listed instances — so a template can omit
|
|
85
97
|
`instanceName` whenever exactly one instance is writable, regardless of
|
|
86
98
|
how many are configured. An empty list makes nothing writable. A listed
|
|
87
|
-
name that doesn't exist under `grafana.instances` fails
|
|
88
|
-
configuration error.
|
|
99
|
+
name that doesn't exist under `grafana.instances` fails backend startup
|
|
100
|
+
with a configuration error (and, defensively, any action run).
|
|
89
101
|
- **`allowOverwrite`** — whether `grafana:dashboard:create` may update
|
|
90
102
|
existing dashboards.
|
|
91
103
|
- `true` (default): the action's `overwrite: true` input updates the
|
package/config.schema.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"items": {
|
|
14
14
|
"type": "string"
|
|
15
15
|
},
|
|
16
|
-
"description": "Which configured Grafana instances the scaffolder actions may write to.\n\n - unset (default): every instance under `grafana.instances` is writable. - a list of instance names: only those instances accept writes. Targeting any other instance fails the action with a 403-style error, and automatic instance selection (when `instanceName` is omitted) only considers the listed instances. A listed name that does not exist under `grafana.instances` fails
|
|
16
|
+
"description": "Which configured Grafana instances the scaffolder actions may write to.\n\n - unset (default): every instance under `grafana.instances` is writable. - a list of instance names: only those instances accept writes. Targeting any other instance fails the action with a 403-style error, and automatic instance selection (when `instanceName` is omitted) only considers the listed instances. A listed name that does not exist under `grafana.instances` fails backend startup with a configuration error (and, defensively, any action run)."
|
|
17
17
|
},
|
|
18
18
|
"allowOverwrite": {
|
|
19
19
|
"type": "boolean",
|
|
@@ -95,33 +95,52 @@ function createGrafanaDashboardCreateAction(options) {
|
|
|
95
95
|
"Updating existing dashboards is disabled by configuration (grafana.scaffolder.allowOverwrite)"
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
|
-
const isUpdate = Boolean(overwrite && uid);
|
|
99
98
|
const collection = `/apis/dashboard.grafana.app/v1/namespaces/${instance.namespace}/dashboards`;
|
|
100
|
-
const
|
|
99
|
+
const itemPath = `${collection}/${encodeURIComponent(uid ?? "")}`;
|
|
100
|
+
const headers = {
|
|
101
|
+
Authorization: `Bearer ${instance.token}`,
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
Accept: "application/json"
|
|
104
|
+
};
|
|
105
|
+
let isUpdate = false;
|
|
106
|
+
let resourceVersion;
|
|
107
|
+
if (overwrite && uid) {
|
|
108
|
+
const current = await fetchApi(`${instance.baseUrl}${itemPath}`, {
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers
|
|
111
|
+
});
|
|
112
|
+
if (current.ok) {
|
|
113
|
+
const currentBody = await current.json();
|
|
114
|
+
resourceVersion = currentBody.metadata?.resourceVersion;
|
|
115
|
+
isUpdate = true;
|
|
116
|
+
} else if (current.status !== 404) {
|
|
117
|
+
throw await errors.ResponseError.fromResponse(current);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
101
120
|
ctx.logger.info(
|
|
102
121
|
`${isUpdate ? "Updating" : "Creating"} Grafana dashboard '${title}' in instance '${instance.name}'`
|
|
103
122
|
);
|
|
104
|
-
const response = await fetchApi(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
123
|
+
const response = await fetchApi(
|
|
124
|
+
`${instance.baseUrl}${isUpdate ? itemPath : collection}`,
|
|
125
|
+
{
|
|
126
|
+
method: isUpdate ? "PUT" : "POST",
|
|
127
|
+
headers,
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
metadata: {
|
|
130
|
+
...uid ? { name: uid } : {},
|
|
131
|
+
...resourceVersion ? { resourceVersion } : {},
|
|
132
|
+
...folderUid ? { annotations: { "grafana.app/folder": folderUid } } : {}
|
|
133
|
+
},
|
|
134
|
+
spec: {
|
|
135
|
+
title,
|
|
136
|
+
schemaVersion: 41,
|
|
137
|
+
tags: tags ?? [],
|
|
138
|
+
panels: [],
|
|
139
|
+
...dashboard ?? {}
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
);
|
|
125
144
|
if (!response.ok) {
|
|
126
145
|
throw await errors.ResponseError.fromResponse(response);
|
|
127
146
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createDashboard.cjs.js","sources":["../../src/actions/createDashboard.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport {\n InputError,\n NotAllowedError,\n NotFoundError,\n ResponseError,\n} from '@backstage/errors';\nimport { createTemplateAction } from '@backstage/plugin-scaffolder-node';\nimport {\n FetchApi,\n GrafanaInstanceConfig,\n readGrafanaInstances,\n slugify,\n} from '@marble-sh/backstage-plugin-grafana-node';\n\ntype ScaffolderGuardConfig = {\n allowedInstances?: string[];\n allowOverwrite: boolean;\n};\n\nfunction readGuardConfig(rootConfig: Config): ScaffolderGuardConfig {\n const config = rootConfig.getOptionalConfig('grafana.scaffolder');\n return {\n allowedInstances: config?.getOptionalStringArray('allowedInstances'),\n allowOverwrite: config?.getOptionalBoolean('allowOverwrite') ?? true,\n };\n}\n\nfunction resolveInstance(\n instances: GrafanaInstanceConfig[],\n guard: ScaffolderGuardConfig,\n name?: string,\n): GrafanaInstanceConfig {\n const { allowedInstances } = guard;\n if (allowedInstances) {\n const known = new Set(instances.map(instance => instance.name));\n const unknown = allowedInstances.filter(allowed => !known.has(allowed));\n if (unknown.length > 0) {\n throw new InputError(\n `grafana.scaffolder.allowedInstances names unknown instance(s) '${unknown.join(\n \"', '\",\n )}'; configured instances are: ${[...known].join(', ')}`,\n );\n }\n }\n\n if (name) {\n const found = instances.find(instance => instance.name === name);\n if (!found) {\n throw new NotFoundError(\n `No Grafana instance configured with name '${name}'`,\n );\n }\n if (allowedInstances && !allowedInstances.includes(found.name)) {\n throw new NotAllowedError(\n `Grafana instance '${found.name}' is not writable by the scaffolder (grafana.scaffolder.allowedInstances)`,\n );\n }\n return found;\n }\n\n // Automatic selection only considers writable instances.\n const writable = allowedInstances\n ? instances.filter(instance => allowedInstances.includes(instance.name))\n : instances;\n if (writable.length === 0) {\n throw new InputError(\n 'No Grafana instances are configured and writable by the scaffolder',\n );\n }\n if (writable.length > 1) {\n throw new InputError(\n 'Multiple Grafana instances are configured; set input.instanceName to choose one',\n );\n }\n return writable[0];\n}\n\n/**\n * Creates the `grafana:dashboard:create` scaffolder action, which creates (or\n * updates) a dashboard in a configured Grafana instance via the App Platform\n * `dashboard.grafana.app/v1` API.\n *\n * @public\n */\nexport function createGrafanaDashboardCreateAction(options: {\n config: Config;\n fetch?: FetchApi;\n}) {\n const fetchApi = options.fetch ?? fetch;\n\n return createTemplateAction({\n id: 'grafana:dashboard:create',\n description:\n 'Creates or updates a Grafana dashboard via the App Platform API',\n schema: {\n input: {\n instanceName: z =>\n z\n .string({\n description:\n 'The configured Grafana instance to target. Optional when only one instance is configured.',\n })\n .optional(),\n title: z => z.string({ description: 'The dashboard title' }),\n uid: z =>\n z\n .string({\n description:\n 'The dashboard uid (metadata.name). When omitted, Grafana generates one.',\n })\n .optional(),\n folderUid: z =>\n z\n .string({ description: 'The uid of the folder to create it in' })\n .optional(),\n tags: z =>\n z.array(z.string(), { description: 'Dashboard tags' }).optional(),\n dashboard: z =>\n z\n .record(z.any(), {\n description:\n 'Additional dashboard spec fields (panels, templating, etc.), merged into the request spec.',\n })\n .optional(),\n overwrite: z =>\n z\n .boolean({\n description:\n 'Update the dashboard if it already exists (requires uid).',\n })\n .optional(),\n },\n output: {\n uid: z => z.string({ description: 'The uid of the dashboard' }),\n url: z => z.string({ description: 'The URL of the dashboard' }),\n instanceName: z =>\n z.string({\n description: 'The instance the dashboard was created in',\n }),\n },\n },\n async handler(ctx) {\n const { title, uid, folderUid, tags, dashboard, overwrite } = ctx.input;\n const guard = readGuardConfig(options.config);\n const instance = resolveInstance(\n readGrafanaInstances(options.config),\n guard,\n ctx.input.instanceName,\n );\n\n if (overwrite && !guard.allowOverwrite) {\n throw new NotAllowedError(\n 'Updating existing dashboards is disabled by configuration (grafana.scaffolder.allowOverwrite)',\n );\n }\n\n const isUpdate = Boolean(overwrite && uid);\n const collection = `/apis/dashboard.grafana.app/v1/namespaces/${instance.namespace}/dashboards`;\n const path = isUpdate ? `${collection}/${uid}` : collection;\n\n ctx.logger.info(\n `${\n isUpdate ? 'Updating' : 'Creating'\n } Grafana dashboard '${title}' in instance '${instance.name}'`,\n );\n\n const response = await fetchApi(`${instance.baseUrl}${path}`, {\n method: isUpdate ? 'PUT' : 'POST',\n headers: {\n Authorization: `Bearer ${instance.token}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n metadata: {\n ...(uid ? { name: uid } : {}),\n ...(folderUid\n ? { annotations: { 'grafana.app/folder': folderUid } }\n : {}),\n },\n spec: {\n title,\n schemaVersion: 41,\n tags: tags ?? [],\n panels: [],\n ...(dashboard ?? {}),\n },\n }),\n });\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const body = (await response.json()) as { metadata?: { name?: string } };\n const resultUid = body.metadata?.name ?? uid ?? '';\n const url = `${instance.baseUrl}/d/${resultUid}/${slugify(title)}`;\n\n ctx.output('uid', resultUid);\n ctx.output('url', url);\n ctx.output('instanceName', instance.name);\n },\n });\n}\n"],"names":["InputError","NotFoundError","NotAllowedError","createTemplateAction","readGrafanaInstances","ResponseError","slugify"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createDashboard.cjs.js","sources":["../../src/actions/createDashboard.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport {\n InputError,\n NotAllowedError,\n NotFoundError,\n ResponseError,\n} from '@backstage/errors';\nimport { createTemplateAction } from '@backstage/plugin-scaffolder-node';\nimport {\n FetchApi,\n GrafanaInstanceConfig,\n readGrafanaInstances,\n slugify,\n} from '@marble-sh/backstage-plugin-grafana-node';\n\ntype ScaffolderGuardConfig = {\n allowedInstances?: string[];\n allowOverwrite: boolean;\n};\n\nfunction readGuardConfig(rootConfig: Config): ScaffolderGuardConfig {\n const config = rootConfig.getOptionalConfig('grafana.scaffolder');\n return {\n allowedInstances: config?.getOptionalStringArray('allowedInstances'),\n allowOverwrite: config?.getOptionalBoolean('allowOverwrite') ?? true,\n };\n}\n\nfunction resolveInstance(\n instances: GrafanaInstanceConfig[],\n guard: ScaffolderGuardConfig,\n name?: string,\n): GrafanaInstanceConfig {\n const { allowedInstances } = guard;\n if (allowedInstances) {\n const known = new Set(instances.map(instance => instance.name));\n const unknown = allowedInstances.filter(allowed => !known.has(allowed));\n if (unknown.length > 0) {\n throw new InputError(\n `grafana.scaffolder.allowedInstances names unknown instance(s) '${unknown.join(\n \"', '\",\n )}'; configured instances are: ${[...known].join(', ')}`,\n );\n }\n }\n\n if (name) {\n const found = instances.find(instance => instance.name === name);\n if (!found) {\n throw new NotFoundError(\n `No Grafana instance configured with name '${name}'`,\n );\n }\n if (allowedInstances && !allowedInstances.includes(found.name)) {\n throw new NotAllowedError(\n `Grafana instance '${found.name}' is not writable by the scaffolder (grafana.scaffolder.allowedInstances)`,\n );\n }\n return found;\n }\n\n // Automatic selection only considers writable instances.\n const writable = allowedInstances\n ? instances.filter(instance => allowedInstances.includes(instance.name))\n : instances;\n if (writable.length === 0) {\n throw new InputError(\n 'No Grafana instances are configured and writable by the scaffolder',\n );\n }\n if (writable.length > 1) {\n throw new InputError(\n 'Multiple Grafana instances are configured; set input.instanceName to choose one',\n );\n }\n return writable[0];\n}\n\n/**\n * Creates the `grafana:dashboard:create` scaffolder action, which creates (or\n * updates) a dashboard in a configured Grafana instance via the App Platform\n * `dashboard.grafana.app/v1` API.\n *\n * @public\n */\nexport function createGrafanaDashboardCreateAction(options: {\n config: Config;\n fetch?: FetchApi;\n}) {\n const fetchApi = options.fetch ?? fetch;\n\n return createTemplateAction({\n id: 'grafana:dashboard:create',\n description:\n 'Creates or updates a Grafana dashboard via the App Platform API',\n schema: {\n input: {\n instanceName: z =>\n z\n .string({\n description:\n 'The configured Grafana instance to target. Optional when only one instance is configured.',\n })\n .optional(),\n title: z => z.string({ description: 'The dashboard title' }),\n uid: z =>\n z\n .string({\n description:\n 'The dashboard uid (metadata.name). When omitted, Grafana generates one.',\n })\n .optional(),\n folderUid: z =>\n z\n .string({ description: 'The uid of the folder to create it in' })\n .optional(),\n tags: z =>\n z.array(z.string(), { description: 'Dashboard tags' }).optional(),\n dashboard: z =>\n z\n .record(z.any(), {\n description:\n 'Additional dashboard spec fields (panels, templating, etc.), merged into the request spec.',\n })\n .optional(),\n overwrite: z =>\n z\n .boolean({\n description:\n 'Update the dashboard if it already exists (requires uid).',\n })\n .optional(),\n },\n output: {\n uid: z => z.string({ description: 'The uid of the dashboard' }),\n url: z => z.string({ description: 'The URL of the dashboard' }),\n instanceName: z =>\n z.string({\n description: 'The instance the dashboard was created in',\n }),\n },\n },\n async handler(ctx) {\n const { title, uid, folderUid, tags, dashboard, overwrite } = ctx.input;\n const guard = readGuardConfig(options.config);\n const instance = resolveInstance(\n readGrafanaInstances(options.config),\n guard,\n ctx.input.instanceName,\n );\n\n if (overwrite && !guard.allowOverwrite) {\n throw new NotAllowedError(\n 'Updating existing dashboards is disabled by configuration (grafana.scaffolder.allowOverwrite)',\n );\n }\n\n const collection = `/apis/dashboard.grafana.app/v1/namespaces/${instance.namespace}/dashboards`;\n const itemPath = `${collection}/${encodeURIComponent(uid ?? '')}`;\n const headers = {\n Authorization: `Bearer ${instance.token}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n\n // An update must carry the current resourceVersion (Kubernetes-style\n // optimistic concurrency). When the dashboard does not exist yet, an\n // `overwrite` run degrades to a plain create, keeping template runs\n // idempotent.\n let isUpdate = false;\n let resourceVersion: string | undefined;\n if (overwrite && uid) {\n const current = await fetchApi(`${instance.baseUrl}${itemPath}`, {\n method: 'GET',\n headers,\n });\n if (current.ok) {\n const currentBody = (await current.json()) as {\n metadata?: { resourceVersion?: string };\n };\n resourceVersion = currentBody.metadata?.resourceVersion;\n isUpdate = true;\n } else if (current.status !== 404) {\n throw await ResponseError.fromResponse(current);\n }\n }\n\n ctx.logger.info(\n `${\n isUpdate ? 'Updating' : 'Creating'\n } Grafana dashboard '${title}' in instance '${instance.name}'`,\n );\n\n const response = await fetchApi(\n `${instance.baseUrl}${isUpdate ? itemPath : collection}`,\n {\n method: isUpdate ? 'PUT' : 'POST',\n headers,\n body: JSON.stringify({\n metadata: {\n ...(uid ? { name: uid } : {}),\n ...(resourceVersion ? { resourceVersion } : {}),\n ...(folderUid\n ? { annotations: { 'grafana.app/folder': folderUid } }\n : {}),\n },\n spec: {\n title,\n schemaVersion: 41,\n tags: tags ?? [],\n panels: [],\n ...(dashboard ?? {}),\n },\n }),\n },\n );\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const body = (await response.json()) as { metadata?: { name?: string } };\n const resultUid = body.metadata?.name ?? uid ?? '';\n const url = `${instance.baseUrl}/d/${resultUid}/${slugify(title)}`;\n\n ctx.output('uid', resultUid);\n ctx.output('url', url);\n ctx.output('instanceName', instance.name);\n },\n });\n}\n"],"names":["InputError","NotFoundError","NotAllowedError","createTemplateAction","readGrafanaInstances","ResponseError","slugify"],"mappings":"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"}
|
package/dist/module.cjs.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var errors = require('@backstage/errors');
|
|
4
5
|
var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
|
|
6
|
+
var backstagePluginGrafanaNode = require('@marble-sh/backstage-plugin-grafana-node');
|
|
5
7
|
var createDashboard = require('./actions/createDashboard.cjs.js');
|
|
6
8
|
|
|
9
|
+
function assertValidGuardConfig(rootConfig) {
|
|
10
|
+
const allowedInstances = rootConfig.getOptionalConfig("grafana.scaffolder")?.getOptionalStringArray("allowedInstances");
|
|
11
|
+
if (!allowedInstances) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const known = new Set(
|
|
15
|
+
backstagePluginGrafanaNode.readGrafanaInstances(rootConfig).map((instance) => instance.name)
|
|
16
|
+
);
|
|
17
|
+
const unknown = allowedInstances.filter((name) => !known.has(name));
|
|
18
|
+
if (unknown.length > 0) {
|
|
19
|
+
throw new errors.InputError(
|
|
20
|
+
`grafana.scaffolder.allowedInstances names unknown instance(s) '${unknown.join(
|
|
21
|
+
"', '"
|
|
22
|
+
)}'; configured instances are: ${[...known].join(", ")}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
7
26
|
const scaffolderModuleGrafana = backendPluginApi.createBackendModule({
|
|
8
27
|
moduleId: "grafana",
|
|
9
28
|
pluginId: "scaffolder",
|
|
@@ -14,6 +33,7 @@ const scaffolderModuleGrafana = backendPluginApi.createBackendModule({
|
|
|
14
33
|
config: backendPluginApi.coreServices.rootConfig
|
|
15
34
|
},
|
|
16
35
|
async init({ scaffolder, config }) {
|
|
36
|
+
assertValidGuardConfig(config);
|
|
17
37
|
scaffolder.addActions(createDashboard.createGrafanaDashboardCreateAction({ config }));
|
|
18
38
|
}
|
|
19
39
|
});
|
package/dist/module.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';\nimport { createGrafanaDashboardCreateAction } from './actions';\n\n/**\n * Scaffolder backend module that registers Grafana provisioning actions.\n *\n * @public\n */\nexport const scaffolderModuleGrafana = createBackendModule({\n moduleId: 'grafana',\n pluginId: 'scaffolder',\n register(env) {\n env.registerInit({\n deps: {\n scaffolder: scaffolderActionsExtensionPoint,\n config: coreServices.rootConfig,\n },\n async init({ scaffolder, config }) {\n scaffolder.addActions(createGrafanaDashboardCreateAction({ config }));\n },\n });\n },\n});\n"],"names":["createBackendModule","scaffolderActionsExtensionPoint","coreServices","createGrafanaDashboardCreateAction"],"mappings":"
|
|
1
|
+
{"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport { InputError } from '@backstage/errors';\nimport { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';\nimport { Config } from '@backstage/config';\nimport { readGrafanaInstances } from '@marble-sh/backstage-plugin-grafana-node';\nimport { createGrafanaDashboardCreateAction } from './actions';\n\n/**\n * Validates `grafana.scaffolder.allowedInstances` against `grafana.instances`\n * at startup, so a configuration typo fails the backend boot (like the catalog\n * module does) instead of every scaffolder run.\n */\nfunction assertValidGuardConfig(rootConfig: Config): void {\n const allowedInstances = rootConfig\n .getOptionalConfig('grafana.scaffolder')\n ?.getOptionalStringArray('allowedInstances');\n if (!allowedInstances) {\n return;\n }\n const known = new Set(\n readGrafanaInstances(rootConfig).map(instance => instance.name),\n );\n const unknown = allowedInstances.filter(name => !known.has(name));\n if (unknown.length > 0) {\n throw new InputError(\n `grafana.scaffolder.allowedInstances names unknown instance(s) '${unknown.join(\n \"', '\",\n )}'; configured instances are: ${[...known].join(', ')}`,\n );\n }\n}\n\n/**\n * Scaffolder backend module that registers Grafana provisioning actions.\n *\n * @public\n */\nexport const scaffolderModuleGrafana = createBackendModule({\n moduleId: 'grafana',\n pluginId: 'scaffolder',\n register(env) {\n env.registerInit({\n deps: {\n scaffolder: scaffolderActionsExtensionPoint,\n config: coreServices.rootConfig,\n },\n async init({ scaffolder, config }) {\n assertValidGuardConfig(config);\n scaffolder.addActions(createGrafanaDashboardCreateAction({ config }));\n },\n });\n },\n});\n"],"names":["readGrafanaInstances","InputError","createBackendModule","scaffolderActionsExtensionPoint","coreServices","createGrafanaDashboardCreateAction"],"mappings":";;;;;;;;AA+BA,SAAS,uBAAuB,UAAA,EAA0B;AACxD,EAAA,MAAM,mBAAmB,UAAA,CACtB,iBAAA,CAAkB,oBAAoB,CAAA,EACrC,uBAAuB,kBAAkB,CAAA;AAC7C,EAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,IAAA;AAAA,EACF;AACA,EAAA,MAAM,QAAQ,IAAI,GAAA;AAAA,IAChBA,gDAAqB,UAAU,CAAA,CAAE,GAAA,CAAI,CAAA,QAAA,KAAY,SAAS,IAAI;AAAA,GAChE;AACA,EAAA,MAAM,OAAA,GAAU,iBAAiB,MAAA,CAAO,CAAA,IAAA,KAAQ,CAAC,KAAA,CAAM,GAAA,CAAI,IAAI,CAAC,CAAA;AAChE,EAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,kEAAkE,OAAA,CAAQ,IAAA;AAAA,QACxE;AAAA,OACD,gCAAgC,CAAC,GAAG,KAAK,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,KACxD;AAAA,EACF;AACF;AAOO,MAAM,0BAA0BC,oCAAA,CAAoB;AAAA,EACzD,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,YAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,UAAA,EAAYC,oDAAA;AAAA,QACZ,QAAQC,6BAAA,CAAa;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,UAAA,EAAY,QAAO,EAAG;AACjC,QAAA,sBAAA,CAAuB,MAAM,CAAA;AAC7B,QAAA,UAAA,CAAW,UAAA,CAAWC,kDAAA,CAAmC,EAAE,MAAA,EAAQ,CAAC,CAAA;AAAA,MACtE;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marble-sh/backstage-plugin-scaffolder-backend-module-grafana",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Scaffolder backend module with actions for provisioning Grafana dashboards",
|
|
5
5
|
"main": "./dist/index.cjs.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -27,17 +27,17 @@
|
|
|
27
27
|
"test": "backstage-cli package test"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@backstage/backend-plugin-api": "
|
|
31
|
-
"@backstage/config": "
|
|
32
|
-
"@backstage/errors": "
|
|
33
|
-
"@backstage/plugin-scaffolder-node": "
|
|
34
|
-
"@marble-sh/backstage-plugin-grafana-node": "
|
|
30
|
+
"@backstage/backend-plugin-api": "^1.10.0",
|
|
31
|
+
"@backstage/config": "^1.3.8",
|
|
32
|
+
"@backstage/errors": "^1.3.1",
|
|
33
|
+
"@backstage/plugin-scaffolder-node": "^0.13.6",
|
|
34
|
+
"@marble-sh/backstage-plugin-grafana-node": "^1.2.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@backstage/backend-test-utils": "
|
|
38
|
-
"@backstage/cli": "
|
|
39
|
-
"@backstage/plugin-scaffolder-node-test-utils": "
|
|
40
|
-
"@backstage/types": "
|
|
37
|
+
"@backstage/backend-test-utils": "^1.11.6",
|
|
38
|
+
"@backstage/cli": "^0.36.5",
|
|
39
|
+
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.14",
|
|
40
|
+
"@backstage/types": "^1.2.2"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|
|
@@ -74,4 +74,4 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"configSchema": "config.schema.json"
|
|
77
|
-
}
|
|
77
|
+
}
|