From 196579fa606a587c2ed1c2515944fbd44f57a81d Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 14 Jul 2026 14:20:45 +0200 Subject: [PATCH] feat(backendbinary): require a linux/amd64 backend binary --- .../passes/backendbinary/backendbinary.go | 48 +++++- .../backendbinary/backendbinary_test.go | 157 +++++++++++++++++- 2 files changed, 196 insertions(+), 9 deletions(-) diff --git a/pkg/analysis/passes/backendbinary/backendbinary.go b/pkg/analysis/passes/backendbinary/backendbinary.go index f0d8628c..3a45635e 100644 --- a/pkg/analysis/passes/backendbinary/backendbinary.go +++ b/pkg/analysis/passes/backendbinary/backendbinary.go @@ -1,6 +1,7 @@ package backendbinary import ( + "debug/buildinfo" "fmt" "path/filepath" @@ -25,6 +26,10 @@ var ( Name: "alerting-found-but-backend-false", Severity: analysis.Error, } + backendBinaryNotLinuxAmd64 = &analysis.Rule{ + Name: "backend-binary-not-linux-amd64", + Severity: analysis.Error, + } ) var Analyzer = &analysis.Analyzer{ @@ -35,6 +40,7 @@ var Analyzer = &analysis.Analyzer{ backendBinaryMissing, backendFoundButNotDeclared, alertingFoundButBackendFalse, + backendBinaryNotLinuxAmd64, }, ReadmeInfo: analysis.ReadmeInfo{ Name: "Backend Binary", @@ -99,7 +105,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // no executable in plugin.json skipping other checks if data.Executable == "" { - return nil, nil + continue } executable := data.Executable @@ -129,7 +135,47 @@ func run(pass *analysis.Pass) (interface{}, error) { ) return nil, nil } + + // backend true with binaries found but none targeting linux/amd64 + if data.Backend && len(foundBinaries) > 0 && !hasLinuxAmd64Binary(foundBinaries) { + pass.ReportResult( + pass.AnalyzerName, + backendBinaryNotLinuxAmd64, + "Missing linux/amd64 backend binary", + fmt.Sprintf( + "You have declared a backend component in %s but none of the backend binaries target linux/amd64. Grafana requires a linux/amd64 backend binary. Please include one.", + pluginJsonPath, + ), + ) + return nil, nil + } } return nil, nil } + +// hasLinuxAmd64Binary reports whether any of the given binaries is a Go binary +// built for linux/amd64, determined from the embedded build info (GOOS/GOARCH) +// rather than the file name. +func hasLinuxAmd64Binary(binaries []string) bool { + for _, binaryPath := range binaries { + info, err := buildinfo.ReadFile(binaryPath) + if err != nil { + logme.Debugln("Could not read build info for", binaryPath, err) + continue + } + var goos, goarch string + for _, setting := range info.Settings { + switch setting.Key { + case "GOOS": + goos = setting.Value + case "GOARCH": + goarch = setting.Value + } + } + if goos == "linux" && goarch == "amd64" { + return true + } + } + return false +} diff --git a/pkg/analysis/passes/backendbinary/backendbinary_test.go b/pkg/analysis/passes/backendbinary/backendbinary_test.go index 9030e4be..c089595e 100644 --- a/pkg/analysis/passes/backendbinary/backendbinary_test.go +++ b/pkg/analysis/passes/backendbinary/backendbinary_test.go @@ -1,6 +1,8 @@ package backendbinary import ( + "os" + "os/exec" "path/filepath" "testing" @@ -175,10 +177,13 @@ func TestBackendTrueExecutablesFound(t *testing.T) { meta, err := testutils.JSONToMetadata(pluginJsonContent) require.NoError(t, err) + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_linux_amd64"), "linux", "amd64") + pass := &analysis.Pass{ - RootDir: filepath.Join("testdata", "missing"), + RootDir: archiveDir, ResultOf: map[*analysis.Analyzer]interface{}{ - archive.Analyzer: filepath.Join("testdata", "found"), + archive.Analyzer: archiveDir, nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ "plugin.json": meta, }, @@ -210,10 +215,14 @@ func TestBackendTrueNested(t *testing.T) { nestedMeta, err := testutils.JSONToMetadata(nestedPluginJsonContent) require.NoError(t, err) + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_linux_amd64"), "linux", "amd64") + buildGoBinary(t, filepath.Join(archiveDir, "datasource", "gpx_plugin_linux_amd64"), "linux", "amd64") + pass := &analysis.Pass{ - RootDir: filepath.Join("testdata", "missing"), + RootDir: archiveDir, ResultOf: map[*analysis.Analyzer]interface{}{ - archive.Analyzer: filepath.Join("testdata", "nested", "found"), + archive.Analyzer: archiveDir, nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ "plugin.json": meta, "datasource/plugin.json": nestedMeta, @@ -244,10 +253,13 @@ func TestBackendTrueOnlyNestedBinary(t *testing.T) { nestedMeta, err := testutils.JSONToMetadata(nestedPluginJsonContent) require.NoError(t, err) + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "datasource", "gpx_plugin_linux_amd64"), "linux", "amd64") + pass := &analysis.Pass{ - RootDir: filepath.Join("testdata", "missing"), + RootDir: archiveDir, ResultOf: map[*analysis.Analyzer]interface{}{ - archive.Analyzer: filepath.Join("testdata", "nested", "found"), + archive.Analyzer: archiveDir, nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ "plugin.json": meta, "datasource/plugin.json": nestedMeta, @@ -280,10 +292,14 @@ func TestBackendMissingNestedDatasource(t *testing.T) { nestedMeta, err := testutils.JSONToMetadata(nestedPluginJsonContent) require.NoError(t, err) + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_linux_amd64"), "linux", "amd64") + require.NoError(t, os.MkdirAll(filepath.Join(archiveDir, "datasource"), 0o755)) + pass := &analysis.Pass{ - RootDir: filepath.Join("testdata", "missing"), + RootDir: archiveDir, ResultOf: map[*analysis.Analyzer]interface{}{ - archive.Analyzer: filepath.Join("testdata", "nested", "missing"), + archive.Analyzer: archiveDir, nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ "plugin.json": meta, "datasource/plugin.json": nestedMeta, @@ -333,3 +349,128 @@ func TestBackendFalseNested(t *testing.T) { require.NoError(t, err) require.Len(t, interceptor.Diagnostics, 0) } + +func TestBackendTrueBinaryNotLinuxAmd64(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pluginJsonContent := []byte(`{ + "name": "my plugin name", + "backend": true, + "executable": "gpx_plugin" + }`) + + meta, err := testutils.JSONToMetadata(pluginJsonContent) + require.NoError(t, err) + + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_darwin_arm64"), "darwin", "arm64") + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_windows_amd64.exe"), "windows", "amd64") + + pass := &analysis.Pass{ + RootDir: archiveDir, + ResultOf: map[*analysis.Analyzer]interface{}{ + archive.Analyzer: archiveDir, + nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ + "plugin.json": meta, + }, + }, + Report: interceptor.ReportInterceptor(), + } + + _, err = Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 1) + require.Equal( + t, + "Missing linux/amd64 backend binary", + interceptor.Diagnostics[0].Title, + ) +} + +func TestBackendTrueLinuxAmd64AmongMultiplePlatforms(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pluginJsonContent := []byte(`{ + "name": "my plugin name", + "backend": true, + "executable": "gpx_plugin" + }`) + + meta, err := testutils.JSONToMetadata(pluginJsonContent) + require.NoError(t, err) + + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_darwin_arm64"), "darwin", "arm64") + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_windows_amd64.exe"), "windows", "amd64") + buildGoBinary(t, filepath.Join(archiveDir, "gpx_plugin_linux_amd64"), "linux", "amd64") + + pass := &analysis.Pass{ + RootDir: archiveDir, + ResultOf: map[*analysis.Analyzer]interface{}{ + archive.Analyzer: archiveDir, + nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ + "plugin.json": meta, + }, + }, + Report: interceptor.ReportInterceptor(), + } + + _, err = Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 0) +} + +func TestBackendFrontendDoesNotSkipNestedLinuxAmd64Check(t *testing.T) { + frontendPluginJson := []byte(`{ + "name": "my plugin name" + }`) + nestedPluginJson := []byte(`{ + "backend": true, + "executable": "gpx_plugin" + }`) + + frontendMeta, err := testutils.JSONToMetadata(frontendPluginJson) + require.NoError(t, err) + nestedMeta, err := testutils.JSONToMetadata(nestedPluginJson) + require.NoError(t, err) + + archiveDir := t.TempDir() + buildGoBinary(t, filepath.Join(archiveDir, "datasource", "gpx_plugin_darwin_arm64"), "darwin", "arm64") + + // Map iteration order is unspecified, so run repeatedly: the nested backend + // without a linux/amd64 binary must ALWAYS be reported, never skipped + // because the frontend entry happened to be visited first. + for i := 0; i < 50; i++ { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: archiveDir, + ResultOf: map[*analysis.Analyzer]interface{}{ + archive.Analyzer: archiveDir, + nestedmetadata.Analyzer: nestedmetadata.Metadatamap{ + "plugin.json": frontendMeta, + "datasource/plugin.json": nestedMeta, + }, + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 1, "run %d: nested backend check was skipped", i) + require.Equal(t, "Missing linux/amd64 backend binary", interceptor.Diagnostics[0].Title) + } +} + +// buildGoBinary cross-compiles a minimal Go program for the given GOOS/GOARCH +// so tests exercise the real build-info detection instead of relying on file +// names or empty placeholder files. +func buildGoBinary(t *testing.T, destPath, goos, goarch string) { + t.Helper() + modDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(modDir, "go.mod"), []byte("module testbin\n\ngo 1.21\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(modDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644)) + require.NoError(t, os.MkdirAll(filepath.Dir(destPath), 0o755)) + cmd := exec.Command("go", "build", "-o", destPath, ".") + cmd.Dir = modDir + cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch, "CGO_ENABLED=0") + out, err := cmd.CombinedOutput() + require.NoError(t, err, "failed to build test binary: %s", out) +}