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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion pkg/analysis/passes/backendbinary/backendbinary.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package backendbinary

import (
"debug/buildinfo"
"fmt"
"path/filepath"

Expand All @@ -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{
Expand All @@ -35,6 +40,7 @@ var Analyzer = &analysis.Analyzer{
backendBinaryMissing,
backendFoundButNotDeclared,
alertingFoundButBackendFalse,
backendBinaryNotLinuxAmd64,
},
ReadmeInfo: analysis.ReadmeInfo{
Name: "Backend Binary",
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixes a bug, we were potentially skipping nested backend binaries.

}

executable := data.Executable
Expand Down Expand Up @@ -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
}
157 changes: 149 additions & 8 deletions pkg/analysis/passes/backendbinary/backendbinary_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package backendbinary

import (
"os"
"os/exec"
"path/filepath"
"testing"

Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Loading