diff --git a/.prettierignore b/.prettierignore
index cc9c39938ad..62700e93ea4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -33,3 +33,6 @@ public/openapi3.json
# Generated Kinds report
kinds/report.json
+
+# Generated schema docs
+docs/sources/developers/kinds/
diff --git a/docs/Makefile b/docs/Makefile
index 4f553a5e878..96dc60c463f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,24 +1,28 @@
.PHONY: pull docs docs-quick docs-no-pull docs-test docs-local-static
+PODMAN = $(shell if command -v podman &>/dev/null; then echo podman; else echo docker; fi)
IMAGE = grafana/docs-base:latest
CONTENT_PATH = /hugo/content/docs/grafana/next
LOCAL_STATIC_PATH = ../../website/static
PORT = 3002:3002
pull:
- docker pull $(IMAGE)
+ $(PODMAN) pull $(IMAGE)
docs: pull
- docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
-
+ $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
+
+docs-preview: pull
+ $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server BUILD_DRAFTS=true"
+
docs-no-pull:
- docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
+ $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
docs-test: pull
- docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod'
+ $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod'
# expects that you have grafana/website checked out in same path as the grafana repo.
docs-local-static: pull
if [ ! -d "$(LOCAL_STATIC_PATH)" ]; then echo "local path (website project) $(LOCAL_STATIC_PATH) not found"]; exit 1; fi
- docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \
+ $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \
-v $(shell pwd)/$(LOCAL_STATIC_PATH):/hugo/static:Z -p $(PORT) --rm -it $(IMAGE)
diff --git a/docs/sources/developers/kinds/_index.md b/docs/sources/developers/kinds/_index.md
new file mode 100644
index 00000000000..cdb07012a0c
--- /dev/null
+++ b/docs/sources/developers/kinds/_index.md
@@ -0,0 +1,10 @@
+---
+title: Grafana schema
+weight: 200
+_build:
+ list: false
+---
+
+# Grafana schema
+
+{{< section >}}
diff --git a/docs/sources/developers/kinds/core/_index.md b/docs/sources/developers/kinds/core/_index.md
new file mode 100644
index 00000000000..9e1eedc72aa
--- /dev/null
+++ b/docs/sources/developers/kinds/core/_index.md
@@ -0,0 +1,10 @@
+---
+title: Core kinds
+weight: 200
+---
+
+# Grafana core kinds
+
+Kinds that define Grafana’s core schematized object types - dashboards, datasources, users, etc.
+
+{{< section >}}
diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md
new file mode 100644
index 00000000000..5fc5f9be873
--- /dev/null
+++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md
@@ -0,0 +1,219 @@
+---
+keywords:
+ - grafana
+ - schema
+title: Dashboard kind
+---
+
+# Dashboard kind
+
+### Maturity: merged
+### Version: 0.0
+
+## Properties
+
+| Property | Type | Required | Description |
+|------------------------|-----------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `editable` | boolean | **Yes** | Whether a dashboard is editable or not. Default: `true`. |
+| `graphTooltip` | integer | **Yes** | 0 for no shared crosshair or tooltip (default).
1 for shared crosshair.
2 for shared crosshair AND shared tooltip. Possible values are: `0`, `1`, `2`. Default: `0`. |
+| `revision` | integer | **Yes** | Version of the current dashboard data Default: `-1`. |
+| `schemaVersion` | integer | **Yes** | Version of the JSON schema, incremented each time a Grafana update brings
changes to said schema.
TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion Default: `36`. |
+| `style` | string | **Yes** | Theme of dashboard. Possible values are: `dark`, `light`. Default: `dark`. |
+| `annotations` | [object](#annotations) | No | TODO docs |
+| `description` | string | No | Description of dashboard. |
+| `fiscalYearStartMonth` | integer | No | TODO docs |
+| `gnetId` | string | No | |
+| `id` | integer | No | Unique numeric identifier for the dashboard.
TODO must isolate or remove identifiers local to a Grafana instance...? |
+| `links` | [DashboardLink](#dashboardlink)[] | No | TODO docs |
+| `liveNow` | boolean | No | TODO docs |
+| `panels` | [object](#panels)[] | No | |
+| `refresh` | | No | TODO docs |
+| `snapshot` | [Snapshot](#snapshot) | No | TODO docs |
+| `tags` | string[] | No | Tags associated with dashboard. |
+| `templating` | [object](#templating) | No | TODO docs |
+| `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc |
+| `timepicker` | [object](#timepicker) | No | TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes |
+| `timezone` | string | No | Timezone of dashboard, Possible values are: `browser`, `utc`, ``. Default: `browser`. |
+| `title` | string | No | Title of dashboard. |
+| `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) |
+| `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. |
+| `weekStart` | string | No | TODO docs |
+
+## DashboardLink
+
+FROM public/app/features/dashboard/state/DashboardModels.ts - ish
+TODO docs
+
+### Properties
+
+| Property | Type | Required | Description |
+|---------------|----------|----------|------------------------------------------------------|
+| `asDropdown` | boolean | **Yes** | Default: `false`. |
+| `icon` | string | **Yes** | |
+| `includeVars` | boolean | **Yes** | Default: `false`. |
+| `keepTime` | boolean | **Yes** | Default: `false`. |
+| `tags` | string[] | **Yes** | |
+| `targetBlank` | boolean | **Yes** | Default: `false`. |
+| `title` | string | **Yes** | |
+| `tooltip` | string | **Yes** | |
+| `type` | string | **Yes** | TODO docs Possible values are: `link`, `dashboards`. |
+| `url` | string | **Yes** | |
+
+## Snapshot
+
+TODO docs
+
+### Properties
+
+| Property | Type | Required | Description |
+|---------------|---------|----------|-------------|
+| `created` | string | **Yes** | TODO docs |
+| `expires` | string | **Yes** | TODO docs |
+| `externalUrl` | string | **Yes** | TODO docs |
+| `external` | boolean | **Yes** | TODO docs |
+| `id` | integer | **Yes** | TODO docs |
+| `key` | string | **Yes** | TODO docs |
+| `name` | string | **Yes** | TODO docs |
+| `orgId` | integer | **Yes** | TODO docs |
+| `updated` | string | **Yes** | TODO docs |
+| `userId` | integer | **Yes** | TODO docs |
+| `url` | string | No | TODO docs |
+
+## annotations
+
+TODO docs
+
+### Properties
+
+| Property | Type | Required | Description |
+|----------|---------------------------------------|----------|-------------|
+| `list` | [AnnotationQuery](#annotationquery)[] | No | |
+
+### AnnotationQuery
+
+TODO docs
+FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
+
+#### Properties
+
+| Property | Type | Required | Description |
+|--------------|---------------------------------------|----------|-------------------------------------------------|
+| `builtIn` | integer | **Yes** | Default: `0`. |
+| `datasource` | [object](#datasource) | **Yes** | Datasource to use for annotation. |
+| `enable` | boolean | **Yes** | Whether annotation is enabled. Default: `true`. |
+| `showIn` | integer | **Yes** | Default: `0`. |
+| `type` | string | **Yes** | Default: `dashboard`. |
+| `hide` | boolean | No | Whether to hide annotation. Default: `false`. |
+| `iconColor` | string | No | Annotation icon color. |
+| `name` | string | No | Name of annotation. |
+| `rawQuery` | string | No | Query for annotation data. |
+| `target` | [AnnotationTarget](#annotationtarget) | No | TODO docs |
+
+#### AnnotationTarget
+
+TODO docs
+
+##### Properties
+
+| Property | Type | Required | Description |
+|------------|----------|----------|-------------|
+| `limit` | integer | **Yes** | |
+| `matchAny` | boolean | **Yes** | |
+| `tags` | string[] | **Yes** | |
+| `type` | string | **Yes** | |
+
+#### datasource
+
+Datasource to use for annotation.
+
+##### Properties
+
+| Property | Type | Required | Description |
+|----------|--------|----------|-------------|
+| `type` | string | No | |
+| `uid` | string | No | |
+
+## panels
+
+| Property | Type | Required | Description |
+|----------|------|----------|-------------|
+
+## templating
+
+TODO docs
+
+### Properties
+
+| Property | Type | Required | Description |
+|----------|-----------------------------------|----------|-------------|
+| `list` | [VariableModel](#variablemodel)[] | No | |
+
+### VariableModel
+
+FROM: packages/grafana-data/src/types/templateVars.ts
+TODO docs
+TODO what about what's in public/app/features/types.ts?
+TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction
+
+#### Properties
+
+| Property | Type | Required | Description |
+|----------------|---------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `global` | boolean | **Yes** | Default: `false`. |
+| `hide` | integer | **Yes** | Possible values are: `0`, `1`, `2`. |
+| `id` | string | **Yes** | Default: `00000000-0000-0000-0000-000000000000`. |
+| `index` | integer | **Yes** | Default: `-1`. |
+| `name` | string | **Yes** | |
+| `skipUrlSync` | boolean | **Yes** | Default: `false`. |
+| `state` | string | **Yes** | Possible values are: `NotStarted`, `Loading`, `Streaming`, `Done`, `Error`. |
+| `type` | string | **Yes** | FROM: packages/grafana-data/src/types/templateVars.ts
TODO docs
TODO this implies some wider pattern/discriminated union, probably? Possible values are: `query`, `adhoc`, `constant`, `datasource`, `interval`, `textbox`, `custom`, `system`. |
+| `datasource` | [DataSourceRef](#datasourceref) | No | Ref to a DataSource instance |
+| `description` | string | No | |
+| `error` | [object](#error) | No | |
+| `label` | string | No | |
+| `query` | | No | TODO: Move this into a separated QueryVariableModel type |
+| `rootStateKey` | string | No | |
+
+#### DataSourceRef
+
+Ref to a DataSource instance
+
+##### Properties
+
+| Property | Type | Required | Description |
+|----------|--------|----------|------------------------------|
+| `type` | string | No | The plugin type-id |
+| `uid` | string | No | Specific datasource instance |
+
+#### error
+
+| Property | Type | Required | Description |
+|----------|------|----------|-------------|
+
+## time
+
+Time range for dashboard, e.g. last 6 hours, last 7 days, etc
+
+### Properties
+
+| Property | Type | Required | Description |
+|----------|--------|----------|--------------------|
+| `from` | string | **Yes** | Default: `now-6h`. |
+| `to` | string | **Yes** | Default: `now`. |
+
+## timepicker
+
+TODO docs
+TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes
+
+### Properties
+
+| Property | Type | Required | Description |
+|---------------------|----------|----------|----------------------------------------------------------------------------------------|
+| `collapse` | boolean | **Yes** | Whether timepicker is collapsed or not. Default: `false`. |
+| `enable` | boolean | **Yes** | Whether timepicker is enabled or not. Default: `true`. |
+| `hidden` | boolean | **Yes** | Whether timepicker is visible or not. Default: `false`. |
+| `refresh_intervals` | string[] | **Yes** | Selectable intervals for auto-refresh. Default: `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]`. |
+| `time_options` | string[] | **Yes** | TODO docs Default: `[5m 15m 1h 6h 12h 24h 2d 7d 30d]`. |
+
+
diff --git a/docs/sources/developers/kinds/core/playlist/schema-reference.md b/docs/sources/developers/kinds/core/playlist/schema-reference.md
new file mode 100644
index 00000000000..918c9cb05ca
--- /dev/null
+++ b/docs/sources/developers/kinds/core/playlist/schema-reference.md
@@ -0,0 +1,32 @@
+---
+keywords:
+ - grafana
+ - schema
+title: Playlist kind
+---
+
+# Playlist kind
+
+### Maturity: merged
+### Version: 0.0
+
+## Properties
+
+| Property | Type | Required | Description |
+|------------|---------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `interval` | string | **Yes** | Interval sets the time between switching views in a playlist.
FIXME: Is this based on a standardized format or what options are available? Can datemath be used? Default: `5m`. |
+| `name` | string | **Yes** | Name of the playlist. |
+| `uid` | string | **Yes** | Unique playlist identifier. Generated on creation, either by the
creator of the playlist of by the application. |
+| `items` | [PlaylistItem](#playlistitem)[] | No | The ordered list of items that the playlist will iterate over.
FIXME! This should not be optional, but changing it makes the godegen awkward |
+
+## PlaylistItem
+
+### Properties
+
+| Property | Type | Required | Description |
+|----------|--------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `type` | string | **Yes** | Type of the item. Possible values are: `dashboard_by_uid`, `dashboard_by_id`, `dashboard_by_tag`. |
+| `value` | string | **Yes** | Value depends on type and describes the playlist item.
- dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
is not portable as the numerical identifier is non-deterministic between different instances.
Will be replaced by dashboard_by_uid in the future. (deprecated)
- dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
dashboards behind the tag will be added to the playlist.
- dashboard_by_uid: The value is the dashboard UID |
+| `title` | string | No | Title is an unused property -- it will be removed in the future |
+
+
diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md
new file mode 100644
index 00000000000..6da29527078
--- /dev/null
+++ b/docs/sources/developers/kinds/core/team/schema-reference.md
@@ -0,0 +1,34 @@
+---
+keywords:
+ - grafana
+ - schema
+title: Team kind
+---
+
+# Team kind
+
+### Maturity: merged
+### Version: 0.0
+
+## Properties
+
+| Property | Type | Required | Description |
+|-----------------|--------------------------|----------|----------------------------------------------------------|
+| `created` | integer | **Yes** | Created indicates when the team was created. |
+| `memberCount` | integer | **Yes** | MemberCount is the number of the team members. |
+| `name` | string | **Yes** | Name of the team. |
+| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the team belongs to. |
+| `permission` | integer | **Yes** | Possible values are: `0`, `1`, `2`, `4`. |
+| `updated` | integer | **Yes** | Updated indicates when the team was updated. |
+| `accessControl` | [object](#accesscontrol) | No | AccessControl metadata associated with a given resource. |
+| `avatarUrl` | string | No | AvatarUrl is the team's avatar URL. |
+| `email` | string | No | Email of the team. |
+
+## accessControl
+
+AccessControl metadata associated with a given resource.
+
+| Property | Type | Required | Description |
+|----------|------|----------|-------------|
+
+
diff --git a/go.mod b/go.mod
index d420673b13d..12483f6bfa8 100644
--- a/go.mod
+++ b/go.mod
@@ -274,6 +274,7 @@ require (
require (
github.com/dave/dst v0.27.2
+ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f
github.com/parca-dev/parca v0.12.1
k8s.io/apimachinery v0.25.3
)
diff --git a/go.sum b/go.sum
index b921e920314..10e99e1a63a 100644
--- a/go.sum
+++ b/go.sum
@@ -2560,9 +2560,12 @@ github.com/xdg/scram v1.0.3/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=
diff --git a/kinds/gen.go b/kinds/gen.go
index 9a06c592c39..3c437c7049d 100644
--- a/kinds/gen.go
+++ b/kinds/gen.go
@@ -40,6 +40,7 @@ func main() {
codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath),
codegen.LatestMajorsOrXJenny(kindsys.TSCoreKindParentPath, codegen.TSTypesJenny{}),
codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")),
+ codegen.DocsJenny(filepath.Join("docs", "sources", "developers", "kinds", "core")),
)
coreKindsGen.AddPostprocessors(codegen.SlashHeaderMapper("kinds/gen.go"))
diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go
index 037766d13d7..1c6d0743f9f 100644
--- a/pkg/codegen/generators.go
+++ b/pkg/codegen/generators.go
@@ -60,7 +60,7 @@ func SlashHeaderMapper(maingen string) codejen.FileMapper {
return func(f codejen.File) (codejen.File, error) {
// Never inject on certain filetypes, it's never valid
switch filepath.Ext(f.RelativePath) {
- case ".json", ".yml", ".yaml":
+ case ".json", ".yml", ".yaml", ".md":
return f, nil
default:
buf := new(bytes.Buffer)
diff --git a/pkg/codegen/jenny_docs.go b/pkg/codegen/jenny_docs.go
new file mode 100644
index 00000000000..a2cb7667ad9
--- /dev/null
+++ b/pkg/codegen/jenny_docs.go
@@ -0,0 +1,490 @@
+package codegen
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "text/template"
+
+ "cuelang.org/go/cue/cuecontext"
+ "github.com/grafana/codejen"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/thema/encoding/jsonschema"
+ "github.com/olekukonko/tablewriter"
+ "github.com/xeipuuv/gojsonpointer"
+)
+
+func DocsJenny(docsPath string) OneToOne {
+ return docsJenny{
+ docsPath: docsPath,
+ }
+}
+
+type docsJenny struct {
+ docsPath string
+}
+
+func (j docsJenny) JennyName() string {
+ return "DocsJenny"
+}
+
+func (j docsJenny) Generate(decl *DeclForGen) (*codejen.File, error) {
+ f, err := jsonschema.GenerateSchema(decl.Lineage().Latest())
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate json representation for the schema: %v", err)
+ }
+ b, err := cuecontext.New().BuildFile(f).MarshalJSON()
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal schema value to json: %v", err)
+ }
+
+ // We don't need entire json obj, only the value of components.schemas path
+ var obj struct {
+ Components struct {
+ Schemas json.RawMessage
+ }
+ }
+ err = json.Unmarshal(b, &obj)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal schema json: %v", err)
+ }
+
+ // fixes the references between the types within a json after making components.schema. the root of the json
+ kindJsonStr := strings.Replace(string(obj.Components.Schemas), "#/components/schemas/", "#/", -1)
+
+ kindProps := decl.Properties.Common()
+ kindName := strings.ToLower(kindProps.Name)
+ data := templateData{
+ KindName: kindProps.Name,
+ KindVersion: decl.Lineage().Latest().Version().String(),
+ KindMaturity: string(kindProps.Maturity),
+ Markdown: "{{ .Markdown 1 }}",
+ }
+
+ tmpl, err := makeTemplate(data, "docs.tmpl")
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := jsonToMarkdown([]byte(kindJsonStr), string(tmpl), kindName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build markdown for kind %s: %v", kindName, err)
+ }
+
+ return codejen.NewFile(filepath.Join(j.docsPath, kindName, "schema-reference.md"), doc, j), nil
+}
+
+// makeTemplate pre-populates the template with the kind metadata
+func makeTemplate(data templateData, tmpl string) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ if err := tmpls.Lookup(tmpl).Execute(buf, data); err != nil {
+ return []byte{}, fmt.Errorf("failed to populate docs template with the kind metadata")
+ }
+ return buf.Bytes(), nil
+}
+
+type templateData struct {
+ KindName string
+ KindVersion string
+ KindMaturity string
+ Markdown string
+}
+
+// -------------------- JSON to Markdown conversion --------------------
+// Copied from https://github.com/marcusolsson/json-schema-docs and slightly changed to fit the DocsJenny
+
+type schema struct {
+ ID string `json:"$id,omitempty"`
+ Ref string `json:"$ref,omitempty"`
+ Schema string `json:"$schema,omitempty"`
+ Title string `json:"title,omitempty"`
+ Description string `json:"description,omitempty"`
+ Required []string `json:"required,omitempty"`
+ Type PropertyTypes `json:"type,omitempty"`
+ Properties map[string]*schema `json:"properties,omitempty"`
+ Items *schema `json:"items,omitempty"`
+ Definitions map[string]*schema `json:"definitions,omitempty"`
+ Enum []Any `json:"enum"`
+ Default any `json:"default"`
+}
+
+func jsonToMarkdown(jsonData []byte, tpl string, kindName string) ([]byte, error) {
+ sch, err := newSchema(jsonData, kindName)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ t, err := template.New("markdown").Parse(tpl)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ buf := new(bytes.Buffer)
+ err = t.Execute(buf, sch)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+func newSchema(b []byte, kindName string) (*schema, error) {
+ var data map[string]*schema
+ if err := json.Unmarshal(b, &data); err != nil {
+ return nil, err
+ }
+
+ // Needed for resolving in-schema references.
+ root, err := simplejson.NewJson(b)
+ if err != nil {
+ return nil, err
+ }
+
+ return resolveSchema(data[kindName], root)
+}
+
+// resolveSchema recursively resolves schemas.
+func resolveSchema(schem *schema, root *simplejson.Json) (*schema, error) {
+ for _, prop := range schem.Properties {
+ if prop.Ref != "" {
+ tmp, err := resolveReference(prop.Ref, root)
+ if err != nil {
+ return nil, err
+ }
+ *prop = *tmp
+ }
+ foo, err := resolveSchema(prop, root)
+ if err != nil {
+ return nil, err
+ }
+ *prop = *foo
+ }
+
+ if schem.Items != nil {
+ if schem.Items.Ref != "" {
+ tmp, err := resolveReference(schem.Items.Ref, root)
+ if err != nil {
+ return nil, err
+ }
+ *schem.Items = *tmp
+ }
+ foo, err := resolveSchema(schem.Items, root)
+ if err != nil {
+ return nil, err
+ }
+ *schem.Items = *foo
+ }
+
+ return schem, nil
+}
+
+// resolveReference loads a schema from a $ref.
+// If ref contains a hashtag (#), the part after represents a in-schema reference.
+func resolveReference(ref string, root *simplejson.Json) (*schema, error) {
+ i := strings.Index(ref, "#")
+
+ if i != 0 {
+ return nil, fmt.Errorf("not in-schema reference: %s", ref)
+ }
+ return resolveInSchemaReference(ref[i+1:], root)
+}
+
+func resolveInSchemaReference(ref string, root *simplejson.Json) (*schema, error) {
+ // in-schema reference
+ pointer, err := gojsonpointer.NewJsonPointer(ref)
+ if err != nil {
+ return nil, err
+ }
+
+ v, _, err := pointer.Get(root.MustMap())
+ if err != nil {
+ return nil, err
+ }
+
+ var sch schema
+ b, err := json.Marshal(v)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := json.Unmarshal(b, &sch); err != nil {
+ return nil, err
+ }
+
+ // Set the ref name as title
+ sch.Title = path.Base(ref)
+
+ return &sch, nil
+}
+
+// Markdown returns the Markdown representation of the schema.
+//
+// The level argument can be used to offset the heading levels. This can be
+// useful if you want to add the schema under a subheading.
+func (s schema) Markdown(level int) string {
+ if level < 1 {
+ level = 1
+ }
+
+ var buf bytes.Buffer
+
+ if s.Title != "" {
+ fmt.Fprintln(&buf, makeHeading(s.Title, level))
+ fmt.Fprintln(&buf)
+ }
+
+ if s.Description != "" {
+ fmt.Fprintln(&buf, s.Description)
+ if s.Default != nil {
+ fmt.Fprintf(&buf, "The default value is: `%v`.", s.Default)
+ }
+ fmt.Fprintln(&buf)
+ }
+
+ if len(s.Properties) > 0 {
+ fmt.Fprintln(&buf, makeHeading("Properties", level+1))
+ fmt.Fprintln(&buf)
+ }
+
+ printProperties(&buf, &s)
+
+ // Add padding.
+ fmt.Fprintln(&buf)
+
+ for _, obj := range findDefinitions(&s) {
+ fmt.Fprint(&buf, obj.Markdown(level+1))
+ }
+
+ return buf.String()
+}
+
+func makeHeading(heading string, level int) string {
+ if level < 0 {
+ return heading
+ }
+
+ if level <= 6 {
+ return strings.Repeat("#", level) + " " + heading
+ }
+
+ return fmt.Sprintf("**%s**", heading)
+}
+
+func findDefinitions(s *schema) []*schema {
+ // Gather all properties of object type so that we can generate the
+ // properties for them recursively.
+ var objs []*schema
+
+ for k, p := range s.Properties {
+ // Use the identifier as the title.
+ if p.Type.HasType(PropertyTypeObject) {
+ if len(p.Title) == 0 {
+ p.Title = k
+ }
+ objs = append(objs, p)
+ }
+
+ // If the property is an array of objects, use the name of the array
+ // property as the title.
+ if p.Type.HasType(PropertyTypeArray) {
+ if p.Items != nil {
+ if p.Items.Type.HasType(PropertyTypeObject) {
+ if len(p.Items.Title) == 0 {
+ p.Items.Title = k
+ }
+ objs = append(objs, p.Items)
+ }
+ }
+ }
+ }
+
+ // Sort the object schemas.
+ sort.Slice(objs, func(i, j int) bool {
+ return objs[i].Title < objs[j].Title
+ })
+
+ return objs
+}
+
+func printProperties(w io.Writer, s *schema) {
+ table := tablewriter.NewWriter(w)
+ table.SetHeader([]string{"Property", "Type", "Required", "Description"})
+ table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
+ table.SetCenterSeparator("|")
+ table.SetAutoFormatHeaders(false)
+ table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+ table.SetAutoWrapText(false)
+
+ // Buffer all property rows so that we can sort them before printing them.
+ var rows [][]string
+
+ for k, p := range s.Properties {
+ // Generate relative links for objects and arrays of objects.
+ var propType []string
+ for _, pt := range p.Type {
+ switch pt {
+ case PropertyTypeObject:
+ name, anchor := propNameAndAnchor(k, p.Title)
+ propType = append(propType, fmt.Sprintf("[%s](#%s)", name, anchor))
+ case PropertyTypeArray:
+ if p.Items != nil {
+ for _, pi := range p.Items.Type {
+ if pi == PropertyTypeObject {
+ name, anchor := propNameAndAnchor(k, p.Items.Title)
+ propType = append(propType, fmt.Sprintf("[%s](#%s)[]", name, anchor))
+ } else {
+ propType = append(propType, fmt.Sprintf("%s[]", pi))
+ }
+ }
+ } else {
+ propType = append(propType, string(pt))
+ }
+ default:
+ propType = append(propType, string(pt))
+ }
+ }
+
+ var propTypeStr string
+ if len(propType) == 1 {
+ propTypeStr = propType[0]
+ } else if len(propType) == 2 {
+ propTypeStr = strings.Join(propType, " or ")
+ } else if len(propType) > 2 {
+ propTypeStr = fmt.Sprintf("%s, or %s", strings.Join(propType[:len(propType)-1], ", "), propType[len(propType)-1])
+ }
+
+ // Emphasize required properties.
+ var required string
+ if in(s.Required, k) {
+ required = "**Yes**"
+ } else {
+ required = "No"
+ }
+
+ desc := p.Description
+
+ if len(p.Enum) > 0 {
+ var vals []string
+ for _, e := range p.Enum {
+ vals = append(vals, e.String())
+ }
+ desc += " Possible values are: `" + strings.Join(vals, "`, `") + "`."
+ }
+
+ if p.Default != nil {
+ desc += fmt.Sprintf(" Default: `%v`.", p.Default)
+ }
+
+ rows = append(rows, []string{fmt.Sprintf("`%s`", k), propTypeStr, required, formatForTable(desc)})
+ }
+
+ // Sort by the required column, then by the name column.
+ sort.Slice(rows, func(i, j int) bool {
+ if rows[i][2] < rows[j][2] {
+ return true
+ }
+ if rows[i][2] > rows[j][2] {
+ return false
+ }
+ return rows[i][0] < rows[j][0]
+ })
+
+ table.AppendBulk(rows)
+ table.Render()
+}
+
+func propNameAndAnchor(prop, title string) (string, string) {
+ if len(title) > 0 {
+ return title, strings.ToLower(title)
+ }
+ return string(PropertyTypeObject), strings.ToLower(prop)
+}
+
+// in returns true if a string slice contains a specific string.
+func in(strs []string, str string) bool {
+ for _, s := range strs {
+ if s == str {
+ return true
+ }
+ }
+ return false
+}
+
+// formatForTable returns string usable in a Markdown table.
+// It trims white spaces, replaces new lines and pipe characters.
+func formatForTable(in string) string {
+ s := strings.TrimSpace(in)
+ s = strings.ReplaceAll(s, "\n", "
")
+ s = strings.ReplaceAll(s, "|", "|")
+ return s
+}
+
+type PropertyTypes []PropertyType
+
+func (pts *PropertyTypes) HasType(pt PropertyType) bool {
+ for _, t := range *pts {
+ if t == pt {
+ return true
+ }
+ }
+ return false
+}
+
+func (pts *PropertyTypes) UnmarshalJSON(data []byte) error {
+ var value interface{}
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+
+ switch val := value.(type) {
+ case string:
+ *pts = []PropertyType{PropertyType(val)}
+ return nil
+ case []interface{}:
+ var pt []PropertyType
+ for _, t := range val {
+ s, ok := t.(string)
+ if !ok {
+ return errors.New("unsupported property type")
+ }
+ pt = append(pt, PropertyType(s))
+ }
+ *pts = pt
+ default:
+ return errors.New("unsupported property type")
+ }
+
+ return nil
+}
+
+type PropertyType string
+
+const (
+ PropertyTypeString PropertyType = "string"
+ PropertyTypeNumber PropertyType = "number"
+ PropertyTypeBoolean PropertyType = "boolean"
+ PropertyTypeObject PropertyType = "object"
+ PropertyTypeArray PropertyType = "array"
+ PropertyTypeNull PropertyType = "null"
+)
+
+type Any struct {
+ value interface{}
+}
+
+func (u *Any) UnmarshalJSON(data []byte) error {
+ if err := json.Unmarshal(data, &u.value); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *Any) String() string {
+ return fmt.Sprintf("%v", u.value)
+}
diff --git a/pkg/codegen/tmpl/docs.tmpl b/pkg/codegen/tmpl/docs.tmpl
new file mode 100644
index 00000000000..7f135c97cea
--- /dev/null
+++ b/pkg/codegen/tmpl/docs.tmpl
@@ -0,0 +1,13 @@
+---
+keywords:
+ - grafana
+ - schema
+title: {{ .KindName }} kind
+---
+
+# {{ .KindName }} kind
+
+### Maturity: {{ .KindMaturity }}
+### Version: {{ .KindVersion }}
+
+{{ .Markdown }}