diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca8ca64a..66daccf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,3 +10,51 @@ Thank you for your interest in contributing to this repository. We are glad you ## Next steps - [Set up your development environment](./docs/developer-guide.md) + +## Data source configuration schema + +[`pkg/schema/dsconfig.json`](./pkg/schema/dsconfig.json) describes the plugin's configuration fields, storage locations, validation rules, UI hints, and instructions for automation. Edit this file when changing the configuration schema. + +The format and shared tooling come from [`grafana/dsconfig`](https://github.com/grafana/dsconfig/tree/main/dsconfig): + +- [README](https://github.com/grafana/dsconfig/tree/main/dsconfig#readme): concepts and examples. +- [Schema reference](https://github.com/grafana/dsconfig/blob/main/dsconfig/schema.md): field properties and validation rules. +- [Publishing guide](https://github.com/grafana/dsconfig/blob/main/dsconfig/PUBLISH-SCHEMA.md): generation and packaging setup. + +### Source and generated files + +| File | Purpose | +| --- | --- | +| `pkg/schema/dsconfig.json` | Hand-authored configuration schema. | +| `pkg/models/settings.go` | Backend settings model, parsing, and `SecureJsonDataKeys`. | +| `pkg/schema/dsconfig_test.go` | Shared conformance test setup and provisioning examples. | +| `pkg/schema/schema.gen.json` | Generated plugin schema. | +| `pkg/schema/settings.gen.json` | Generated settings schema. | +| `pkg/schema/settings.examples.gen.json` | Generated provisioning examples. | + +Do not edit `.gen.json` files by hand. The generator uses the version of `github.com/grafana/dsconfig/schema` pinned in `go.mod`. The `$schema` URL in `dsconfig.json` identifies the JSON Schema used to validate the authoring format; review that pin when upgrading dsconfig. + +### Updating configuration + +1. Update `pkg/schema/dsconfig.json`, including any relevant groups, validation rules, and instructions. Keep stored fields consistent with the configuration editor and backend. A virtual field represents a UI control rather than a saved setting. +2. Update `models.Settings` and its JSON tags where needed. For secrets, update `models.SecureJsonDataKeys` and the code that reads `DecryptedSecureJSONData` instead of adding a normal JSON setting. Preserve support for existing configurations when changing types, including string and numeric GitHub App IDs. +3. Update `SettingsExamples` in `pkg/schema/dsconfig_test.go` when the configuration examples change. The existing examples cover personal access tokens, GitHub Apps, Enterprise Cloud, and Enterprise Server. Use placeholders, never real credentials. +4. Regenerate the artifacts from the repository root: + + ```bash + go generate ./pkg/schema/... + ``` + +5. Verify schema consistency and backend parsing: + + ```bash + go test ./pkg/schema/... ./pkg/models/... -count=1 + ``` + +6. Review and commit any generated changes alongside the source changes. An instructions-only edit may leave the generated artifacts unchanged. + +The `go:generate` directive in `dsconfig_test.go` runs `go test -run TestPlugin -generateArtifacts`. Normal test runs check the artifacts without regenerating them. A `SchemaArtifactInSync` failure means regeneration is needed; key, type, or secret consistency failures require correcting the schema or Go declarations. + +### Building the published files + +Run `npm run build` to copy the source schema and generated artifacts into the plugin's `dist/schema/` directory via `webpack.config.ts`. This packages the files; it does not replace the Go generation step. Grafana serves the source schema at `/public/plugins/grafana-github-datasource/schema/dsconfig.json` once that build is installed. diff --git a/cspell.config.json b/cspell.config.json index dee63ed0..54ccce36 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -18,6 +18,8 @@ ], "words": [ "apiserver", + "dsconfig", + "dsschema", "dataplane", "araddon", "bmike", @@ -56,6 +58,8 @@ "octocat", "oldorg", "oldrepo", + "pluginschema", + "unconfigured", "prismjs", "promop", "PTRACE", diff --git a/go.mod b/go.mod index 28aac1b2..39641b22 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.26.5 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.19.0 github.com/google/go-github/v84 v84.0.0 + github.com/grafana/dsconfig/schema v0.0.12 github.com/grafana/grafana-plugin-sdk-go v0.296.4 github.com/influxdata/tdigest v0.0.1 github.com/pkg/errors v0.9.1 github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed github.com/stretchr/testify v1.12.1 golang.org/x/oauth2 v0.37.0 + k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad pgregory.net/rapid v1.3.0 ) @@ -21,16 +23,33 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.19.0 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect + github.com/go-openapi/swag v0.28.0 // indirect + github.com/go-openapi/swag/cmdutils v0.28.0 // indirect + github.com/go-openapi/swag/conv v0.28.0 // indirect + github.com/go-openapi/swag/fileutils v0.28.0 // indirect + github.com/go-openapi/swag/jsonutils v0.28.0 // indirect + github.com/go-openapi/swag/loading v0.28.0 // indirect + github.com/go-openapi/swag/mangling v0.28.0 // indirect + github.com/go-openapi/swag/netutils v0.28.0 // indirect + github.com/go-openapi/swag/pools v0.28.0 // indirect + github.com/go-openapi/swag/stringutils v0.28.0 // indirect + github.com/go-openapi/swag/typeutils v0.28.0 // indirect + github.com/go-openapi/swag/yamlutils v0.28.0 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-github/v88 v88.0.0 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/dsconfig/dsconfig v0.0.7 // indirect github.com/grafana/otel-profiling-go v0.6.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.12 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect @@ -38,6 +57,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/jaegertracing/jaeger-idl v0.12.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/jszwedko/go-datemath v0.1.1-0.20260113213115-7f666eef0523 // indirect github.com/klauspost/compress v1.20.0 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect @@ -68,11 +88,14 @@ require ( go.opentelemetry.io/otel/sdk v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 58b15fc7..8a0d9ac2 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v1.9.0 h1:2j3c13lD5v0QTjxphJSSIHS7w8/m/pzSHtLMPOpznC0= github.com/elazarl/goproxy v1.9.0/go.mod h1:THdE5ix2clxX9lZzcICPpZ67d6CdrPZxdOYsNgU5e30= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= @@ -51,6 +53,38 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw= +github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg= +github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q= +github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8= +github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k= +github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU= +github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4= +github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU= +github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k= +github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM= +github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k= +github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= +github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU= +github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU= +github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY= +github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY= +github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= @@ -63,6 +97,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -80,6 +116,10 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grafana/dsconfig/dsconfig v0.0.7 h1:rh2PhwKLVrokMa2eTbS3zDfSIS8sOl54X/as082+Qgo= +github.com/grafana/dsconfig/dsconfig v0.0.7/go.mod h1:orwL9W2Ea8Wjubn0Vw1or/uolFDSKSFfc404G2xrvEc= +github.com/grafana/dsconfig/schema v0.0.12 h1:WQFlKzJtACotG3fcNnU+RcnBMDXxgSAuAphkF2nlyz0= +github.com/grafana/dsconfig/schema v0.0.12/go.mod h1:kBZ/uoNZw6QjYU/eCS2wdBckerbv7zTWLtgy+9Hm8LQ= github.com/grafana/grafana-plugin-sdk-go v0.296.4 h1:3+sYR2lmdb+sIk3m/eA9/lY8sztbhFyGf+6he8nwKuM= github.com/grafana/grafana-plugin-sdk-go v0.296.4/go.mod h1:Dza5QLa5L5/0zH6Ce2CZhfTUd6+3FFDhmEHjT0BEBuM= github.com/grafana/otel-profiling-go v0.6.0 h1:W7lOZaJj4IJISXMcM1UBk3fJF3tzF2OD6MJBJaQp1H8= @@ -106,6 +146,8 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jszwedko/go-datemath v0.1.1-0.20260113213115-7f666eef0523 h1:yho2Mr8EZHU24v7/vMyMc3yPvbs9GDK7TtFRtuMPGqE= +github.com/jszwedko/go-datemath v0.1.1-0.20260113213115-7f666eef0523/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -310,5 +352,15 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc= pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/package.json b/package.json index 9e7dcd74..e64cc53f 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "license": "Apache-2.0", "author": "Grafana Labs (https://grafana.com)", "scripts": { - "build": "webpack -c ./.config/webpack/webpack.config.ts --env production", - "dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development", + "build": "webpack -c ./webpack.config.ts --env production", + "dev": "webpack -w -c ./webpack.config.ts --env development", "e2e": "playwright test", "lint": "eslint --cache .", "lint:fix": "npm run lint -- --fix && prettier --write --list-different .", diff --git a/pkg/github/stargazers_test.go b/pkg/github/stargazers_test.go index b9359a49..a04473bc 100644 --- a/pkg/github/stargazers_test.go +++ b/pkg/github/stargazers_test.go @@ -44,6 +44,10 @@ func TestStargazersDataframe(t *testing.T) { t.Fatal(err) } + // Force a fixed +0000 zone so the rendered time is deterministic across + // environments/Go versions (always "+0000" instead of "UTC"/"GMT"). + starredAt = starredAt.In(time.FixedZone("", 0)) + stargazers := StargazersWrapper{ StargazerWrapper{ Stargazer: Stargazer{ diff --git a/pkg/main.go b/pkg/main.go index 5b4948a3..08451d01 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -6,13 +6,12 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/github-datasource/pkg/models" "github.com/grafana/github-datasource/pkg/plugin" ) -const dsID = "grafana-github-datasource" - func main() { - if err := datasource.Manage(dsID, plugin.NewDataSourceInstance, datasource.ManageOpts{}); err != nil { + if err := datasource.Manage(models.PluginID, plugin.NewDataSourceInstance, datasource.ManageOpts{}); err != nil { backend.Logger.Error(err.Error()) os.Exit(1) } diff --git a/pkg/models/settings.go b/pkg/models/settings.go index 6c06111b..365d9620 100644 --- a/pkg/models/settings.go +++ b/pkg/models/settings.go @@ -7,8 +7,11 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" ) +const PluginID = "grafana-github-datasource" + type AuthType string const ( @@ -18,29 +21,49 @@ const ( type Settings struct { // General settings - GitHubURL string `json:"githubUrl,omitempty"` + GitHubURL string `json:"githubUrl,omitempty"` + // GitHubPlan: Not used in the backend. Adding here for frontend parity + GitHubPlan string `json:"githubPlan,omitempty"` CachingEnabled bool `json:"cachingEnabled,omitempty"` // Auth type related settings SelectedAuthType AuthType `json:"selectedAuthType,omitempty"` // personal-access-token auth related settings AccessToken string // github-app auth related settings - AppId json.RawMessage `json:"appId,omitempty"` + AppId string `json:"appId,omitempty"` // legacy config and provisioning, appId is stored as either number or string. But string should be desired format. So custom UnmarshalJSON function added to support this AppIdInt64 int64 - InstallationId json.RawMessage `json:"installationId,omitempty"` + InstallationId string `json:"installationId,omitempty"` // legacy config and provisioning: installationId may be stored as either number or string; UnmarshalJSON normalizes it to a string InstallationIdInt64 int64 PrivateKey string } +// UnmarshalJSON decodes the settings while tolerating the appId and installationId +// fields being stored either as JSON strings (e.g. "1111") or, for legacy configs, +// as JSON numbers (e.g. 1111). Both are normalized to their string representation. +func (s *Settings) UnmarshalJSON(data []byte) error { + type Alias Settings + aux := struct { + AppId json.RawMessage `json:"appId,omitempty"` + InstallationId json.RawMessage `json:"installationId,omitempty"` + *Alias + }{Alias: (*Alias)(s)} + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + s.AppId = rawMessageToString(aux.AppId, "appId") + s.InstallationId = rawMessageToString(aux.InstallationId, "installationId") + return nil +} + func LoadSettings(settings backend.DataSourceInstanceSettings) (s Settings, err error) { if err := json.Unmarshal(settings.JSONData, &s); err != nil { return s, err } if s.SelectedAuthType == AuthTypeGithubApp { - if s.AppIdInt64, err = rawMessageToInt64(s.AppId, "app id"); err != nil { + if s.AppIdInt64, err = stringToInt64(s.AppId, "app id"); err != nil { return s, err } - if s.InstallationIdInt64, err = rawMessageToInt64(s.InstallationId, "installation id"); err != nil { + if s.InstallationIdInt64, err = stringToInt64(s.InstallationId, "installation id"); err != nil { return s, err } if val, ok := settings.DecryptedSecureJSONData["privateKey"]; ok { @@ -58,10 +81,39 @@ func LoadSettings(settings backend.DataSourceInstanceSettings) (s Settings, err return s, nil } -func rawMessageToInt64(r json.RawMessage, m string) (out int64, err error) { - out, err = strconv.ParseInt(strings.ReplaceAll(string(r), `"`, ""), 10, 64) +func rawMessageToString(r json.RawMessage, field string) string { + // Keep the original numeric text so large IDs do not lose precision. + if len(r) > 0 && (r[0] == '-' || (r[0] >= '0' && r[0] <= '9')) { + log.DefaultLogger.Warn("datasource jsonData value does not match its declared type", + "field", field, "from", "number", "to", "string", "outcome", "coerced") + } + return strings.Trim(string(r), `"`) +} + +func stringToInt64(v string, m string) (out int64, err error) { + out, err = strconv.ParseInt(v, 10, 64) if err != nil { return 0, fmt.Errorf("error parsing %s", m) } return out, nil } + +type SecureJsonDataKey string + +const ( + // SecureJsonDataKeyAccessToken is set when the user authenticates with a + // GitHub personal access token. + SecureJsonDataKeyAccessToken SecureJsonDataKey = "accessToken" + // SecureJsonDataKeyPrivateKey is set when the user authenticates as a + // GitHub App installation (PEM-encoded RSA private key). + SecureJsonDataKeyPrivateKey SecureJsonDataKey = "privateKey" +) + +// SecureJsonDataConfig lists the secret key names stored in secureJsonData. +type SecureJsonDataConfig []SecureJsonDataKey + +// SecureJsonDataKeys are the secret keys used by the plugin. +var SecureJsonDataKeys = SecureJsonDataConfig{ + SecureJsonDataKeyAccessToken, + SecureJsonDataKeyPrivateKey, +} diff --git a/pkg/models/settings_test.go b/pkg/models/settings_test.go index 8e063b99..4dcdf877 100644 --- a/pkg/models/settings_test.go +++ b/pkg/models/settings_test.go @@ -7,10 +7,57 @@ import ( "github.com/grafana/github-datasource/pkg/models" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type settingsLogger struct { + log.Logger + warnings []map[string]any +} + +func (l *settingsLogger) Warn(_ string, args ...any) { + fields := map[string]any{} + for i := 0; i < len(args); i += 2 { + fields[args[i].(string)] = args[i+1] + } + l.warnings = append(l.warnings, fields) +} + +func TestSettingsIDConversionLogging(t *testing.T) { + previousLogger := log.DefaultLogger + t.Cleanup(func() { log.DefaultLogger = previousLogger }) + for _, tc := range []struct { + name string + jsonData string + appID string + installID string + warnFields []string + }{ + {name: "strings", jsonData: `{"appId":"1111","installationId":"2222"}`, appID: "1111", installID: "2222"}, + {name: "numbers", jsonData: `{"appId":1111,"installationId":2222}`, appID: "1111", installID: "2222", warnFields: []string{"appId", "installationId"}}, + {name: "mixed with large ID", jsonData: `{"appId":9007199254740993,"installationId":"2222"}`, appID: "9007199254740993", installID: "2222", warnFields: []string{"appId"}}, + {name: "missing", jsonData: `{}`}, + {name: "null", jsonData: `{"appId":null,"installationId":null}`, appID: "null", installID: "null"}, + } { + t.Run(tc.name, func(t *testing.T) { + logger := &settingsLogger{Logger: log.NewNullLogger()} + log.DefaultLogger = logger + var settings models.Settings + require.NoError(t, json.Unmarshal([]byte(tc.jsonData), &settings)) + assert.Equal(t, tc.appID, settings.AppId) + assert.Equal(t, tc.installID, settings.InstallationId) + require.Len(t, logger.warnings, len(tc.warnFields)) + for i, field := range tc.warnFields { + assert.Equal(t, map[string]any{ + "field": field, "from": "number", "to": "string", "outcome": "coerced", + }, logger.warnings[i]) + } + }) + } +} + func TestLoadSettings(t *testing.T) { tests := []struct { name string @@ -44,9 +91,9 @@ func TestLoadSettings(t *testing.T) { want: models.Settings{ GitHubURL: "https://foo.com", SelectedAuthType: models.AuthTypeGithubApp, - AppId: []byte(`"1111"`), + AppId: "1111", AppIdInt64: 1111, - InstallationId: []byte(`"2222"`), + InstallationId: "2222", InstallationIdInt64: 2222, PrivateKey: "foo", }, @@ -63,9 +110,9 @@ func TestLoadSettings(t *testing.T) { want: models.Settings{ GitHubURL: "https://foo.com", SelectedAuthType: models.AuthTypeGithubApp, - AppId: []byte(`1111`), + AppId: "1111", AppIdInt64: 1111, - InstallationId: []byte(`2222`), + InstallationId: "2222", InstallationIdInt64: 2222, PrivateKey: "foo", }, diff --git a/pkg/schema/dsconfig.json b/pkg/schema/dsconfig.json new file mode 100644 index 00000000..a7043d72 --- /dev/null +++ b/pkg/schema/dsconfig.json @@ -0,0 +1,296 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/dsconfig/refs/tags/dsconfig/v0.0.12/dsconfig/schema.json", + "schemaVersion": "v1", + "pluginType": "grafana-github-datasource", + "pluginName": "GitHub", + "docURL": "https://grafana.com/docs/plugins/grafana-github-datasource", + "fields": [ + { + "id": "virtual_selectedLicense", + "key": "selectedLicense", + "label": "GitHub License Type", + "valueType": "string", + "kind": "virtual", + "defaultValue": "github-basic", + "validations": [ + { + "type": "allowedValues", + "values": [ + "github-basic", + "github-enterprise-cloud", + "github-enterprise-server" + ] + } + ], + "ui": { + "component": "radio", + "options": [ + { + "label": "Free, Pro & Team", + "value": "github-basic" + }, + { + "label": "Enterprise Cloud", + "value": "github-enterprise-cloud" + }, + { + "label": "Enterprise Server", + "value": "github-enterprise-server" + } + ] + }, + "storage": { + "type": "computed", + "read": "jsonData.githubPlan == 'github-enterprise-server' || jsonData.githubUrl != '' ? 'github-enterprise-server' : (jsonData.githubPlan != '' ? jsonData.githubPlan : 'github-basic')" + }, + "effects": [ + { + "when": "value == 'github-basic'", + "set": { + "jsonData_githubPlan": "github-basic", + "jsonData_githubUrl": "" + } + }, + { + "when": "value == 'github-enterprise-cloud'", + "set": { + "jsonData_githubPlan": "github-enterprise-cloud", + "jsonData_githubUrl": "" + } + }, + { + "when": "value == 'github-enterprise-server'", + "set": { + "jsonData_githubPlan": "github-enterprise-server" + } + } + ] + }, + { + "id": "jsonData_githubPlan", + "key": "githubPlan", + "valueType": "string", + "target": "jsonData", + "validations": [ + { + "type": "allowedValues", + "values": [ + "github-basic", + "github-enterprise-cloud", + "github-enterprise-server" + ] + } + ], + "tags": [ + "managed-by:virtual_selectedLicense", + "frontend-only" + ] + }, + { + "id": "jsonData_githubUrl", + "key": "githubUrl", + "label": "GitHub Enterprise Server URL", + "valueType": "string", + "target": "jsonData", + "role": "endpoint.baseUrl", + "dependsOn": "virtual_selectedLicense == 'github-enterprise-server'", + "requiredWhen": "jsonData_githubPlan == 'github-enterprise-server'", + "ui": { + "component": "input", + "placeholder": "http(s)://HOSTNAME/" + } + }, + { + "id": "jsonData_selectedAuthType", + "key": "selectedAuthType", + "label": "Authentication Type", + "valueType": "string", + "target": "jsonData", + "role": "auth.discriminator", + "defaultValue": "personal-access-token", + "validations": [ + { + "type": "allowedValues", + "values": [ + "personal-access-token", + "github-app" + ] + } + ], + "ui": { + "component": "radio", + "options": [ + { + "label": "Personal Access Token", + "value": "personal-access-token" + }, + { + "label": "GitHub App", + "value": "github-app" + } + ] + } + }, + { + "id": "secureJsonData_accessToken", + "key": "accessToken", + "label": "Personal Access Token", + "valueType": "string", + "target": "secureJsonData", + "role": "auth.bearer.token", + "dependsOn": "jsonData_selectedAuthType == 'personal-access-token'", + "requiredWhen": "jsonData_selectedAuthType == 'personal-access-token'", + "ui": { + "component": "input", + "placeholder": "Personal Access Token" + }, + "help": { + "title": "Access Token & Permissions", + "markdown": "#### How to create a access token\n\nTo create a new fine grained access token, navigate to [Personal Access Tokens](https://github.com/settings/personal-access-tokens/new) or refer the guidelines from [the Github documentation.](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)\n\n#### Repository access\n\nIn the **Repository access** section, Select the required repositories you want to use with the plugin.\n\n#### Permissions\n\nIn the repository permissions, Ensure to provide **read-only access** to the necessary section which you want to use with the plugin. **The plugin does not require any write access.**\nAlong with other permissions such as `Issues`, `Pull Requests`, ensure to provide read-only access to `Meta data` section as well.\nThis plugin does not require any org level permissions", + "docURL": "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token" + } + }, + { + "id": "jsonData_appId", + "key": "appId", + "label": "App ID", + "valueType": "string", + "target": "jsonData", + "dependsOn": "jsonData_selectedAuthType == 'github-app'", + "requiredWhen": "jsonData_selectedAuthType == 'github-app'", + "ui": { + "component": "input", + "placeholder": "App ID" + } + }, + { + "id": "jsonData_installationId", + "key": "installationId", + "label": "Installation ID", + "valueType": "string", + "target": "jsonData", + "dependsOn": "jsonData_selectedAuthType == 'github-app'", + "requiredWhen": "jsonData_selectedAuthType == 'github-app'", + "ui": { + "component": "input", + "placeholder": "Installation ID" + } + }, + { + "id": "secureJsonData_privateKey", + "key": "privateKey", + "label": "Private Key", + "valueType": "string", + "target": "secureJsonData", + "role": "auth.jwt.signingKey", + "dependsOn": "jsonData_selectedAuthType == 'github-app'", + "requiredWhen": "jsonData_selectedAuthType == 'github-app'", + "ui": { + "component": "textarea", + "multiline": true, + "rows": 7, + "placeholder": "-----BEGIN CERTIFICATE-----" + } + }, + { + "id": "jsonData_cachingEnabled", + "key": "cachingEnabled", + "description": "Enables the query caching wrapper in the plugin backend. Not exposed in the configuration editor; the backend currently enables caching for every datasource instance.", + "valueType": "boolean", + "target": "jsonData", + "defaultValue": true, + "tags": [ + "backend-only" + ] + } + ], + "groups": [ + { + "id": "connection", + "title": "Connection", + "order": 1, + "optional": true, + "fieldRefs": [ + "virtual_selectedLicense", + "jsonData_githubUrl" + ] + }, + { + "id": "authentication", + "title": "Authentication", + "order": 2, + "fieldRefs": [ + "jsonData_selectedAuthType", + "secureJsonData_accessToken", + "jsonData_appId", + "jsonData_installationId", + "secureJsonData_privateKey" + ] + } + ], + "relationships": [ + { + "type": "group", + "fields": [ + "jsonData_appId", + "jsonData_installationId", + "secureJsonData_privateKey" + ], + "description": "GitHub App credentials" + }, + { + "type": "pair", + "fields": [ + "jsonData_githubPlan", + "jsonData_githubUrl" + ], + "description": "GitHub license type and the Enterprise Server base URL it enables" + } + ], + "instructions": [ + { + "msg": "Two authentication methods are available, selected via jsonData.selectedAuthType: 'personal-access-token' (the default) and 'github-app'. Configure exactly one method; only the credentials of the selected method are used. Switching methods does not clear the other method's fields or secrets — stale values are harmless but confusing.", + "tags": [ + "llm", + "auth" + ] + }, + { + "msg": "To configure Personal Access Token authentication, set jsonData.selectedAuthType to 'personal-access-token' and put the token in secureJsonData.accessToken. Minimal payload: {\"jsonData\":{\"selectedAuthType\":\"personal-access-token\"},\"secureJsonData\":{\"accessToken\":\"\"}}. Use a fine grained personal access token with read-only repository permissions, including the 'Metadata' permission; no write or org-level permissions are needed.", + "tags": [ + "llm", + "auth" + ] + }, + { + "msg": "To configure GitHub App authentication, set jsonData.selectedAuthType to 'github-app', provide jsonData.appId and jsonData.installationId (numeric values, as JSON strings or numbers), and put the app's private key in secureJsonData.privateKey. Minimal payload: {\"jsonData\":{\"selectedAuthType\":\"github-app\",\"appId\":\"\",\"installationId\":\"\"},\"secureJsonData\":{\"privateKey\":\"\"}}. The private key must be the complete PEM text including the BEGIN/END lines (despite the editor placeholder showing '-----BEGIN CERTIFICATE-----', it is an RSA private key, not a certificate). All three values are mandatory: a missing or non-numeric appId/installationId makes the backend fail to load the datasource.", + "tags": [ + "llm", + "auth" + ] + }, + { + "msg": "Legacy auth interpretation: datasources created before selectedAuthType was introduced store only secureJsonData.accessToken with no auth type; the backend treats them as 'personal-access-token'. Do not assume a missing selectedAuthType means the datasource is unconfigured.", + "tags": [ + "llm", + "auth", + "legacy" + ] + }, + { + "msg": "secureJsonData values (accessToken, privateKey) are write-only: they can be set or reset but never read back. To check whether a secret is already configured on an existing datasource, read the secureJsonFields object (e.g. secureJsonFields.accessToken == true) instead of secureJsonData.", + "tags": [ + "llm", + "auth", + "secure" + ] + }, + { + "msg": "For GitHub.com (Free, Pro & Team) and GitHub Enterprise Cloud, do NOT set jsonData.githubUrl — the backend treats any non-empty githubUrl as an on-prem Enterprise Server. Only set jsonData.githubUrl (with jsonData.githubPlan='github-enterprise-server') for an on-prem instance, e.g. {\"jsonData\":{\"githubPlan\":\"github-enterprise-server\",\"githubUrl\":\"https://github.example.com\"}}, and clear githubUrl to an empty string when switching away. Prefer omitting the trailing slash: the backend string-concatenates '/api/v3' and '/api/graphql' onto githubUrl.", + "tags": [ + "llm", + "connection" + ] + } + ] +} diff --git a/pkg/schema/dsconfig_test.go b/pkg/schema/dsconfig_test.go new file mode 100644 index 00000000..c1c998b2 --- /dev/null +++ b/pkg/schema/dsconfig_test.go @@ -0,0 +1,111 @@ +package schema_test + +import ( + _ "embed" + "testing" + + "github.com/grafana/dsconfig/schema" + "github.com/grafana/grafana-plugin-sdk-go/experimental/pluginschema" + "k8s.io/kube-openapi/pkg/spec3" + + "github.com/grafana/github-datasource/pkg/models" +) + +//go:embed dsconfig.json +var configSchemaJSON []byte + +//go:generate go test -run TestPlugin -generateArtifacts +func TestPlugin(t *testing.T) { + secureKeys := []string{} + for _, k := range models.SecureJsonDataKeys { + secureKeys = append(secureKeys, string(k)) + } + schema.RunPluginTests(t, schema.PluginUnderTest{ + ID: models.PluginID, + ConfigSchemaJSON: configSchemaJSON, + SettingsJSONModel: models.Settings{}, + SecureKeys: secureKeys, + SettingsExamples: &pluginschema.SettingsExamples{ + Examples: map[string]*spec3.Example{ + "": { + ExampleProps: spec3.ExampleProps{ + Summary: "Default configuration", + Description: "The defaults a new datasource starts with: GitHub.com (Free, Pro & Team) and personal access token authentication. The token still has to be supplied before the datasource can load.", + Value: map[string]any{ + "jsonData": map[string]any{ + "githubPlan": "github-basic", + "selectedAuthType": string(models.AuthTypePAT), + }, + "secureJsonData": map[string]any{ + "accessToken": "", + }, + }, + }, + }, + "personalAccessToken": { + ExampleProps: spec3.ExampleProps{ + Summary: "Personal access token on GitHub.com", + Description: "Authenticate against GitHub.com with a fine grained personal access token. Grant read-only repository permissions plus 'Metadata'; the plugin never writes. Do not set jsonData.githubUrl — any non-empty value makes the backend treat the instance as on-prem Enterprise Server.", + Value: map[string]any{ + "jsonData": map[string]any{ + "githubPlan": "github-basic", + "selectedAuthType": string(models.AuthTypePAT), + }, + "secureJsonData": map[string]any{ + "accessToken": "REPLACE_WITH_ACCESS_TOKEN", + }, + }, + }, + }, + "githubApp": { + ExampleProps: spec3.ExampleProps{ + Summary: "GitHub App authentication", + Description: "Authenticate as a GitHub App installation. appId and installationId are numeric values (accepted as JSON strings or numbers) and privateKey is the complete PEM text including the BEGIN/END lines. All three are mandatory: a missing or non-numeric id makes the backend fail to load the datasource.", + Value: map[string]any{ + "jsonData": map[string]any{ + "githubPlan": "github-basic", + "selectedAuthType": string(models.AuthTypeGithubApp), + "appId": "123456", + "installationId": "12345678", + }, + "secureJsonData": map[string]any{ + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----", + }, + }, + }, + }, + "enterpriseCloud": { + ExampleProps: spec3.ExampleProps{ + Summary: "GitHub Enterprise Cloud", + Description: "Enterprise Cloud is still served from github.com, so only jsonData.githubPlan changes. Leave jsonData.githubUrl unset — setting it would route the backend at an on-prem Enterprise Server instead.", + Value: map[string]any{ + "jsonData": map[string]any{ + "githubPlan": "github-enterprise-cloud", + "selectedAuthType": string(models.AuthTypePAT), + }, + "secureJsonData": map[string]any{ + "accessToken": "REPLACE_WITH_ACCESS_TOKEN", + }, + }, + }, + }, + "enterpriseServer": { + ExampleProps: spec3.ExampleProps{ + Summary: "GitHub Enterprise Server (on-prem)", + Description: "Point the datasource at a self-hosted GitHub Enterprise Server. jsonData.githubUrl is required when jsonData.githubPlan is 'github-enterprise-server'. Omit the trailing slash: the backend concatenates '/api/v3' and '/api/graphql' onto the value.", + Value: map[string]any{ + "jsonData": map[string]any{ + "githubPlan": "github-enterprise-server", + "githubUrl": "https://github.example.com", + "selectedAuthType": string(models.AuthTypePAT), + }, + "secureJsonData": map[string]any{ + "accessToken": "REPLACE_WITH_ACCESS_TOKEN", + }, + }, + }, + }, + }, + }, + }) +} diff --git a/pkg/schema/schema.gen.json b/pkg/schema/schema.gen.json new file mode 100644 index 00000000..5179cb24 --- /dev/null +++ b/pkg/schema/schema.gen.json @@ -0,0 +1,131 @@ +{ + "targetApiVersion": "v0alpha1", + "settings": { + "spec": { + "type": "object", + "properties": { + "jsonData": { + "type": "object", + "properties": { + "appId": { + "type": "string", + "x-dsconfig-depends-on": "jsonData_selectedAuthType == 'github-app'", + "x-dsconfig-required-when": "jsonData_selectedAuthType == 'github-app'" + }, + "cachingEnabled": { + "description": "Enables the query caching wrapper in the plugin backend. Not exposed in the configuration editor; the backend currently enables caching for every datasource instance.", + "type": "boolean", + "default": true + }, + "githubPlan": { + "type": "string", + "enum": [ + "github-basic", + "github-enterprise-cloud", + "github-enterprise-server" + ] + }, + "githubUrl": { + "type": "string", + "x-dsconfig-depends-on": "virtual_selectedLicense == 'github-enterprise-server'", + "x-dsconfig-required-when": "jsonData_githubPlan == 'github-enterprise-server'" + }, + "installationId": { + "type": "string", + "x-dsconfig-depends-on": "jsonData_selectedAuthType == 'github-app'", + "x-dsconfig-required-when": "jsonData_selectedAuthType == 'github-app'" + }, + "selectedAuthType": { + "type": "string", + "default": "personal-access-token", + "enum": [ + "personal-access-token", + "github-app" + ] + } + } + } + } + }, + "secureValues": [ + { + "key": "accessToken" + }, + { + "key": "privateKey" + } + ] + }, + "settingsExamples": { + "examples": { + "": { + "summary": "Default configuration", + "description": "The defaults a new datasource starts with: GitHub.com (Free, Pro \u0026 Team) and personal access token authentication. The token still has to be supplied before the datasource can load.", + "value": { + "jsonData": { + "githubPlan": "github-basic", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "" + } + } + }, + "enterpriseCloud": { + "summary": "GitHub Enterprise Cloud", + "description": "Enterprise Cloud is still served from github.com, so only jsonData.githubPlan changes. Leave jsonData.githubUrl unset — setting it would route the backend at an on-prem Enterprise Server instead.", + "value": { + "jsonData": { + "githubPlan": "github-enterprise-cloud", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "REPLACE_WITH_ACCESS_TOKEN" + } + } + }, + "enterpriseServer": { + "summary": "GitHub Enterprise Server (on-prem)", + "description": "Point the datasource at a self-hosted GitHub Enterprise Server. jsonData.githubUrl is required when jsonData.githubPlan is 'github-enterprise-server'. Omit the trailing slash: the backend concatenates '/api/v3' and '/api/graphql' onto the value.", + "value": { + "jsonData": { + "githubPlan": "github-enterprise-server", + "githubUrl": "https://github.example.com", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "REPLACE_WITH_ACCESS_TOKEN" + } + } + }, + "githubApp": { + "summary": "GitHub App authentication", + "description": "Authenticate as a GitHub App installation. appId and installationId are numeric values (accepted as JSON strings or numbers) and privateKey is the complete PEM text including the BEGIN/END lines. All three are mandatory: a missing or non-numeric id makes the backend fail to load the datasource.", + "value": { + "jsonData": { + "appId": "123456", + "githubPlan": "github-basic", + "installationId": "12345678", + "selectedAuthType": "github-app" + }, + "secureJsonData": { + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" + } + } + }, + "personalAccessToken": { + "summary": "Personal access token on GitHub.com", + "description": "Authenticate against GitHub.com with a fine grained personal access token. Grant read-only repository permissions plus 'Metadata'; the plugin never writes. Do not set jsonData.githubUrl — any non-empty value makes the backend treat the instance as on-prem Enterprise Server.", + "value": { + "jsonData": { + "githubPlan": "github-basic", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "REPLACE_WITH_ACCESS_TOKEN" + } + } + } + } + } +} diff --git a/pkg/schema/settings.examples.gen.json b/pkg/schema/settings.examples.gen.json new file mode 100644 index 00000000..16b6de93 --- /dev/null +++ b/pkg/schema/settings.examples.gen.json @@ -0,0 +1,72 @@ +{ + "examples": { + "": { + "summary": "Default configuration", + "description": "The defaults a new datasource starts with: GitHub.com (Free, Pro \u0026 Team) and personal access token authentication. The token still has to be supplied before the datasource can load.", + "value": { + "jsonData": { + "githubPlan": "github-basic", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "" + } + } + }, + "enterpriseCloud": { + "summary": "GitHub Enterprise Cloud", + "description": "Enterprise Cloud is still served from github.com, so only jsonData.githubPlan changes. Leave jsonData.githubUrl unset — setting it would route the backend at an on-prem Enterprise Server instead.", + "value": { + "jsonData": { + "githubPlan": "github-enterprise-cloud", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "REPLACE_WITH_ACCESS_TOKEN" + } + } + }, + "enterpriseServer": { + "summary": "GitHub Enterprise Server (on-prem)", + "description": "Point the datasource at a self-hosted GitHub Enterprise Server. jsonData.githubUrl is required when jsonData.githubPlan is 'github-enterprise-server'. Omit the trailing slash: the backend concatenates '/api/v3' and '/api/graphql' onto the value.", + "value": { + "jsonData": { + "githubPlan": "github-enterprise-server", + "githubUrl": "https://github.example.com", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "REPLACE_WITH_ACCESS_TOKEN" + } + } + }, + "githubApp": { + "summary": "GitHub App authentication", + "description": "Authenticate as a GitHub App installation. appId and installationId are numeric values (accepted as JSON strings or numbers) and privateKey is the complete PEM text including the BEGIN/END lines. All three are mandatory: a missing or non-numeric id makes the backend fail to load the datasource.", + "value": { + "jsonData": { + "appId": "123456", + "githubPlan": "github-basic", + "installationId": "12345678", + "selectedAuthType": "github-app" + }, + "secureJsonData": { + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" + } + } + }, + "personalAccessToken": { + "summary": "Personal access token on GitHub.com", + "description": "Authenticate against GitHub.com with a fine grained personal access token. Grant read-only repository permissions plus 'Metadata'; the plugin never writes. Do not set jsonData.githubUrl — any non-empty value makes the backend treat the instance as on-prem Enterprise Server.", + "value": { + "jsonData": { + "githubPlan": "github-basic", + "selectedAuthType": "personal-access-token" + }, + "secureJsonData": { + "accessToken": "REPLACE_WITH_ACCESS_TOKEN" + } + } + } + } +} diff --git a/pkg/schema/settings.gen.json b/pkg/schema/settings.gen.json new file mode 100644 index 00000000..e17473e3 --- /dev/null +++ b/pkg/schema/settings.gen.json @@ -0,0 +1,56 @@ +{ + "spec": { + "type": "object", + "properties": { + "jsonData": { + "type": "object", + "properties": { + "appId": { + "type": "string", + "x-dsconfig-depends-on": "jsonData_selectedAuthType == 'github-app'", + "x-dsconfig-required-when": "jsonData_selectedAuthType == 'github-app'" + }, + "cachingEnabled": { + "description": "Enables the query caching wrapper in the plugin backend. Not exposed in the configuration editor; the backend currently enables caching for every datasource instance.", + "type": "boolean", + "default": true + }, + "githubPlan": { + "type": "string", + "enum": [ + "github-basic", + "github-enterprise-cloud", + "github-enterprise-server" + ] + }, + "githubUrl": { + "type": "string", + "x-dsconfig-depends-on": "virtual_selectedLicense == 'github-enterprise-server'", + "x-dsconfig-required-when": "jsonData_githubPlan == 'github-enterprise-server'" + }, + "installationId": { + "type": "string", + "x-dsconfig-depends-on": "jsonData_selectedAuthType == 'github-app'", + "x-dsconfig-required-when": "jsonData_selectedAuthType == 'github-app'" + }, + "selectedAuthType": { + "type": "string", + "default": "personal-access-token", + "enum": [ + "personal-access-token", + "github-app" + ] + } + } + } + } + }, + "secureValues": [ + { + "key": "accessToken" + }, + { + "key": "privateKey" + } + ] +} diff --git a/webpack.config.ts b/webpack.config.ts new file mode 100644 index 00000000..1cde7f40 --- /dev/null +++ b/webpack.config.ts @@ -0,0 +1,20 @@ +import type { Configuration } from 'webpack'; +import { merge } from 'webpack-merge'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import grafanaConfig, { Env } from './.config/webpack/webpack.config'; + +const config = async (env: Env): Promise => { + const baseConfig = await grafanaConfig(env); + return merge(baseConfig, { + plugins: [new CopyWebpackPlugin({ + patterns: [ + { from: '../pkg/schema/dsconfig.json', to: './schema/dsconfig.json' }, + { from: '../pkg/schema/schema.gen.json', to: './schema/v0alpha1.json' }, + { from: '../pkg/schema/settings.gen.json', to: './schema/v0alpha1/settings.json' }, + { from: '../pkg/schema/settings.examples.gen.json', to: './schema/v0alpha1/settings.examples.json' }, + ], + })], + }); +}; + +export default config;