diff --git a/README.md b/README.md index 3d9713da..babf03b0 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,7 @@ Run "mage gen:readme" to regenerate this section. | Logos / `logos` | Detects whether the plugin includes small and large logos to display in the plugin catalog. | None | | Manifest (Signing) / `manifest` | When a plugin is signed, the zip file will contain a signed `MANIFEST.txt` file. | None | | Metadata / `metadata` | Checks that `plugin.json` exists and is valid. | None | +| Metadata Grafana Dependency / `grafanadependency` | Checks that dependencies.grafanaDependency in `plugin.json` is valid. | None | | Metadata Paths / `metadatapaths` | Ensures all paths are valid and images referenced exist. | None | | Metadata Validity / `metadatavalid` | Ensures metadata is valid and matches plugin schema. | None | | module.js (exists) / `modulejs` | All plugins require a `module.js` to be loaded. | None | diff --git a/go.mod b/go.mod index 12971fcd..70924190 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/grafana/plugin-validator go 1.25.5 require ( + github.com/Masterminds/semver/v3 v3.4.0 github.com/bmatcuk/doublestar/v4 v4.9.2 github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 github.com/fatih/color v1.18.0 diff --git a/go.sum b/go.sum index f87fc658..628bfa71 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/CycloneDX/cyclonedx-go v0.9.3 h1:Pyk/lwavPz7AaZNvugKFkdWOm93MzaIyWmBw github.com/CycloneDX/cyclonedx-go v0.9.3/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= diff --git a/pkg/analysis/passes/analysis.go b/pkg/analysis/passes/analysis.go index 821561e4..874354cb 100644 --- a/pkg/analysis/passes/analysis.go +++ b/pkg/analysis/passes/analysis.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/plugin-validator/pkg/analysis/passes/discoverability" "github.com/grafana/plugin-validator/pkg/analysis/passes/gomanifest" "github.com/grafana/plugin-validator/pkg/analysis/passes/gosec" + "github.com/grafana/plugin-validator/pkg/analysis/passes/grafanadependency" "github.com/grafana/plugin-validator/pkg/analysis/passes/includesnested" "github.com/grafana/plugin-validator/pkg/analysis/passes/jargon" "github.com/grafana/plugin-validator/pkg/analysis/passes/jssourcemap" @@ -103,4 +104,5 @@ var Analyzers = []*analysis.Analyzer{ virusscan.Analyzer, circulardependencies.Analyzer, codediff.Analyzer, + grafanadependency.Analyzer, } diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go new file mode 100644 index 00000000..0f38c2d0 --- /dev/null +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -0,0 +1,57 @@ +package grafanadependency + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/grafana/plugin-validator/pkg/analysis" + "github.com/grafana/plugin-validator/pkg/analysis/passes/metadata" +) + +var ( + invalidGrafanaDependency = &analysis.Rule{Name: "invalid-grafana-dependency", Severity: analysis.Error} + validGrafanaDependency = &analysis.Rule{Name: "valid-grafana-dependency", Severity: analysis.OK} +) + +var Analyzer = &analysis.Analyzer{ + Name: "grafanadependency", + Requires: []*analysis.Analyzer{metadata.Analyzer}, + Run: run, + Rules: []*analysis.Rule{invalidGrafanaDependency, validGrafanaDependency}, + ReadmeInfo: analysis.ReadmeInfo{ + Name: "Metadata Grafana Dependency", + Description: "Checks that dependencies.grafanaDependency in `plugin.json` is valid.", + }, +} + +func run(pass *analysis.Pass) (interface{}, error) { + metadataBytes, ok := pass.ResultOf[metadata.Analyzer].([]byte) + if !ok { + return nil, nil + } + + var data metadata.Metadata + if err := json.Unmarshal(metadataBytes, &data); err != nil { + // if we fail to unmarshall it means the schema is incorrect + // we will let the metadatavalid validator handle it + return nil, nil + } + + _, err := semver.NewConstraint(data.Dependencies.GrafanaDependency) + if err != nil { + pass.ReportResult( + pass.AnalyzerName, + invalidGrafanaDependency, + fmt.Sprintf("plugin.json: dependencies.grafanaDependency field has invalid or empty version constraint: %q", data.Dependencies.GrafanaDependency), + "The plugin.json file has an invalid or empty grafanaDependency field. Please refer to the documentation for more information. https://grafana.com/docs/grafana/latest/developers/plugins/metadata/#grafanadependency", + ) + return nil, nil + } + + if validGrafanaDependency.ReportAll { + pass.ReportResult(pass.AnalyzerName, validGrafanaDependency, "plugin.json: dependencies.grafanaDependency field is valid", "") + } + + return nil, nil +} diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go new file mode 100644 index 00000000..1584b8a0 --- /dev/null +++ b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go @@ -0,0 +1,68 @@ +package grafanadependency + +import ( + "path/filepath" + "testing" + + "github.com/grafana/plugin-validator/pkg/analysis" + "github.com/grafana/plugin-validator/pkg/analysis/passes/metadata" + "github.com/grafana/plugin-validator/pkg/testpassinterceptor" + "github.com/stretchr/testify/require" +) + +func TestGrafanaDependency(t *testing.T) { + for _, tc := range []struct { + name string + pluginJSON string + titleMsg string + }{ + { + name: "valid grafanaDependency constraint", + pluginJSON: `{ + "id": "test-org-app", + "dependencies": { "grafanaDependency": ">=11.6.0" } + }`, + titleMsg: "", + }, + { + name: "complex but valid grafanaDependency constraint", + pluginJSON: `{ + "id": "test-org-app", + "dependencies": { "grafanaDependency": ">=11.6.11 <12 || >=12.0.10 <12.1 || >=12.1.7 <12.2 || >=12.2.5" } + }`, + titleMsg: "", + }, + { + name: "invalid grafanaDependency constraint", + pluginJSON: `{ + "id": "test-org-app", + "dependencies": { "grafanaDependency": ">=invalid" } + }`, + titleMsg: "plugin.json: dependencies.grafanaDependency field has invalid or empty version constraint: \">=invalid\"", + }, + } { + t.Run(tc.name, func(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]interface{}{ + metadata.Analyzer: []byte(tc.pluginJSON), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + if len(tc.titleMsg) > 0 { + require.Len(t, interceptor.Diagnostics, 1) + require.Equal( + t, + tc.titleMsg, + interceptor.Diagnostics[0].Title, + ) + } else { + require.Len(t, interceptor.Diagnostics, 0) + } + }) + } +} diff --git a/pkg/cmd/plugincheck2/main_test.go b/pkg/cmd/plugincheck2/main_test.go index 0d8a39e7..8cd71385 100644 --- a/pkg/cmd/plugincheck2/main_test.go +++ b/pkg/cmd/plugincheck2/main_test.go @@ -303,6 +303,14 @@ func TestIntegration(t *testing.T) { Name: "code-diff-skipped", }, }, + "grafanadependency": { + { + Severity: "error", + Title: "plugin.json: dependencies.grafanaDependency field has invalid or empty version constraint: \"\"", + Detail: "The plugin.json file has an invalid or empty grafanaDependency field. Please refer to the documentation for more information. https://grafana.com/docs/grafana/latest/developers/plugins/metadata/#grafanadependency", + Name: "invalid-grafana-dependency", + }, + }, }, }, }, diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 12454671..86fc8eb7 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -50,6 +50,7 @@ var tests = []struct { "plugin.json: plugin id should follow the format org-name-type", "LLM review skipped due to errors in metadatavalid", "Code diff skipped due to errors in metadatavalid", + "plugin.json: dependencies.grafanaDependency field has invalid or empty version constraint: \"\"", }}, }