@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 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 if it already exists (requires `uid`). |
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). The
66
- service-account token must have permission to create dashboards.
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 the action with a
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
@@ -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 the action with a configuration error."
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 path = isUpdate ? `${collection}/${uid}` : collection;
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(`${instance.baseUrl}${path}`, {
105
- method: isUpdate ? "PUT" : "POST",
106
- headers: {
107
- Authorization: `Bearer ${instance.token}`,
108
- "Content-Type": "application/json",
109
- Accept: "application/json"
110
- },
111
- body: JSON.stringify({
112
- metadata: {
113
- ...uid ? { name: uid } : {},
114
- ...folderUid ? { annotations: { "grafana.app/folder": folderUid } } : {}
115
- },
116
- spec: {
117
- title,
118
- schemaVersion: 41,
119
- tags: tags ?? [],
120
- panels: [],
121
- ...dashboard ?? {}
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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"}
@@ -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
  });
@@ -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":";;;;;;AA4BO,MAAM,0BAA0BA,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,UAAA,CAAW,UAAA,CAAWC,kDAAA,CAAmC,EAAE,MAAA,EAAQ,CAAC,CAAA;AAAA,MACtE;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}
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.1",
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": "backstage:^",
31
- "@backstage/config": "backstage:^",
32
- "@backstage/errors": "backstage:^",
33
- "@backstage/plugin-scaffolder-node": "backstage:^",
34
- "@marble-sh/backstage-plugin-grafana-node": "workspace:^"
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": "backstage:^",
38
- "@backstage/cli": "backstage:^",
39
- "@backstage/plugin-scaffolder-node-test-utils": "backstage:^",
40
- "@backstage/types": "backstage:^"
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
+ }