Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,063 changes: 6,063 additions & 0 deletions packages/grafana/.generated-specs/alertingNotifications.json

Large diffs are not rendered by default.

5,231 changes: 5,231 additions & 0 deletions packages/grafana/.generated-specs/alertingRules.json

Large diffs are not rendered by default.

4,459 changes: 4,459 additions & 0 deletions packages/grafana/.generated-specs/dashboard.json

Large diffs are not rendered by default.

1,885 changes: 1,885 additions & 0 deletions packages/grafana/.generated-specs/folder.json

Large diffs are not rendered by default.

500 changes: 250 additions & 250 deletions packages/grafana/.generated-specs/grafana.json

Large diffs are not rendered by default.

1,734 changes: 1,734 additions & 0 deletions packages/grafana/.generated-specs/playlist.json

Large diffs are not rendered by default.

90 changes: 89 additions & 1 deletion packages/grafana/scripts/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/**
* convert — turn the Grafana OpenAPI spec into a Smithy 2.0 JSON model.
*
* Input: specs/spec-mirror-grafana/specs/openapi3.json (spec submodule)
* Input: specs/spec-mirror-grafana/specs/*.json (spec submodule)
* patches/*.patch.json (RFC-6902 patches to the OpenAPI document)
* Output: .generated-specs/grafana.json
*
Expand All @@ -13,12 +13,100 @@
import * as path from "node:path";
import { runOpenApiConvert } from "@distilled.cloud/core/codegen/openapi-cli";

type ApiSpec = {
paths?: Record<string, Record<string, unknown>>;
};

const legacyApi = (spec: ApiSpec) => {
spec.paths = Object.fromEntries(
Object.entries(spec.paths ?? {}).map(([route, pathItem]) => [
route.startsWith("/api/") ? route : `/api${route}`,
pathItem,
]),
);
};

const structuredApi = (group: string, version: string) => (spec: ApiSpec) => {
const prefix = `/apis/${group}/${version}`;
const paths: Record<string, Record<string, unknown>> = {};

for (const [route, pathItem] of Object.entries(spec.paths ?? {})) {
const uri =
route === "/" ? prefix : `${prefix}/namespaces/{namespace}${route}`;
paths[uri] = {
...pathItem,
parameters: [
...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
{
name: "namespace",
in: "path",
required: true,
schema: { type: "string" },
},
],
};
}

spec.paths = paths;
};

await runOpenApiConvert({
root: path.resolve(import.meta.dir, ".."),
specs: [
{
name: "grafana",
specPath: "specs/spec-mirror-grafana/specs/openapi3.json",
preprocess: legacyApi,
},
{
name: "dashboard",
specPath: "specs/spec-mirror-grafana/specs/dashboard.grafana.app-v2.json",
preprocess: structuredApi("dashboard.grafana.app", "v2"),
options: {
namespace: "com.grafana.dashboard",
serviceName: "Dashboard",
},
},
{
name: "folder",
specPath: "specs/spec-mirror-grafana/specs/folder.grafana.app-v1.json",
preprocess: structuredApi("folder.grafana.app", "v1"),
options: {
namespace: "com.grafana.folder",
serviceName: "Folder",
},
},
{
name: "playlist",
specPath: "specs/spec-mirror-grafana/specs/playlist.grafana.app-v1.json",
preprocess: structuredApi("playlist.grafana.app", "v1"),
options: {
namespace: "com.grafana.playlist",
serviceName: "Playlist",
},
},
{
name: "alertingRules",
specPath:
"specs/spec-mirror-grafana/specs/rules.alerting.grafana.app-v0alpha1.json",
preprocess: structuredApi("rules.alerting.grafana.app", "v0alpha1"),
options: {
namespace: "com.grafana.alerting.rules",
serviceName: "AlertingRules",
},
},
{
name: "alertingNotifications",
specPath:
"specs/spec-mirror-grafana/specs/notifications.alerting.grafana.app-v1beta1.json",
preprocess: structuredApi(
"notifications.alerting.grafana.app",
"v1beta1",
),
options: {
namespace: "com.grafana.alerting.notifications",
serviceName: "AlertingNotifications",
},
},
],
// OpenAPI-document patches (v0 layout: flat patches/*.patch.json). The
Expand Down
7 changes: 3 additions & 4 deletions packages/grafana/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@
* (`Authorization: Bearer <token>`). The optional `X-Grafana-Org-Id` header
* selects the organization the action applies to.
*
* The published OpenAPI document's server URL is `/api` and every path is
* relative to that prefix, so the protocol appends `/api` to the instance
* origin (`apiBaseUrl`).
* Generated operations include their own `/api` or `/apis` prefix, so
* `apiBaseUrl` is always the Grafana instance origin.
*/
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Redacted from "effect/Redacted";
import { ConfigError } from "@distilled.cloud/core/errors";

/** Grafana OSS default listen address (the instance origin, not `/api`). */
/** Grafana OSS default listen address (the instance origin). */
export const DEFAULT_API_BASE_URL = "http://localhost:3000";

export interface Config {
Expand Down
11 changes: 4 additions & 7 deletions packages/grafana/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*
* request: credentials → `Authorization: Bearer <token>` + optional
* `X-Grafana-Org-Id`, resolved from the calling fiber on every
* request. Paths in the spec are relative to `/api`, so the
* instance origin is joined with that prefix.
* request. The generated models contain their full `/api` or
* `/apis` path, so requests start at the instance origin.
*
* response: 2xx JSON is the payload (sensitive members delivered as
* `Redacted`); non-2xx `{ message?, status? }` bodies map to the
Expand Down Expand Up @@ -41,11 +41,8 @@ export type GrafanaOpError =
/** Context (requirements) shared by every generated Grafana operation. */
export type GrafanaOpContext = Credentials | HttpClient.HttpClient;

/** Join the instance origin with `/api` unless the caller already included it. */
const apiBaseUrl = (origin: string): string => {
const trimmed = origin.replace(/\/+$/, "");
return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
};
/** The models own their route prefix, so credentials only provide an origin. */
const apiBaseUrl = (origin: string): string => origin.replace(/\/+$/, "");

export const GrafanaProtocol: Layer.Layer<API.Protocol> =
makeRestProtocol<Config>({
Expand Down
Loading