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
2 changes: 2 additions & 0 deletions pkg/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type Diagnostic struct {
Name string
}

type Diagnostics map[string][]Diagnostic

type Rule struct {
Name string
Disabled bool
Expand Down
120 changes: 120 additions & 0 deletions pkg/analysis/output/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package output

import (
"bytes"
"encoding/json"

"github.com/fatih/color"

"github.com/grafana/plugin-validator/pkg/analysis"
)

// Marshaler is an interface for encoding the analysis results into bytes.
// Each implementation outputs the analysis results in a different format.
type Marshaler interface {
// Marshal encodes the diagnostics data in the format implemented by the marshaler.
Marshal(data analysis.Diagnostics) ([]byte, error)
}

// marshalerFunc is an adapter for using normal functions as Marshaler.
type marshalerFunc func(data analysis.Diagnostics) ([]byte, error)

// Marshal marshals the diagnostics data using the function.
func (f marshalerFunc) Marshal(data analysis.Diagnostics) ([]byte, error) {
return f(data)
}

// jsonMarshaler is a Marshaler that outputs to JSON format.
type jsonMarshaler struct {
// Additional fields used for JSON output

// id is the plugin ID
id string

// version is the plugin version
version string
}

// NewJSONMarshaler returns a new Marshaler that outputs the diagnostics data in JSON format.
// This marshaler requires additional plugin id and plugin version arguments.
func NewJSONMarshaler(id string, version string) Marshaler {
return jsonMarshaler{id, version}
}

type jsonOutput struct {
ID string `json:"id"`
Version string `json:"version"`
Diagnostics analysis.Diagnostics `json:"plugin-validator"`
}

// Marshal marshals the diagnostics data in JSON format.
// The additional id and version fields are taken from the marshaler itself.
func (j jsonMarshaler) Marshal(data analysis.Diagnostics) ([]byte, error) {
return json.MarshalIndent(jsonOutput{
ID: j.id,
Version: j.version,
Diagnostics: data,
}, "", " ")
}

// MarshalCLI is a Marshaler that returns the diagnostics data in a human-readable format, for CLI usage.
var MarshalCLI = marshalerFunc(func(data analysis.Diagnostics) ([]byte, error) {
var buf bytes.Buffer
for name := range data {
for _, d := range data[name] {
switch d.Severity {
case analysis.Error:
buf.WriteString(color.RedString("error: "))
case analysis.Warning:
buf.WriteString(color.YellowString("warning: "))
case analysis.Recommendation:
buf.WriteString(color.CyanString("recommendation: "))
case analysis.OK:
buf.WriteString(color.GreenString("ok: "))
case analysis.SuspectedProblem:
buf.WriteString(color.YellowString("suspected: "))
}

if d.Context != "" {
buf.WriteString(d.Context + ": ")
}

buf.WriteString(d.Title)
if len(d.Detail) > 0 {
buf.WriteRune('\n')
buf.WriteString(color.BlueString("detail: "))
buf.WriteString(d.Detail)
}
buf.WriteRune('\n')
}
}
return buf.Bytes(), nil
})

// ExitCode returns the exit code of the CLI program.
// It returns:
// 1 if there's an error;
// 1 if there's a warning AND strict is true;
// 0 in all other cases.
func ExitCode(strict bool, diags analysis.Diagnostics) int {
for _, ds := range diags {
for _, d := range ds {
switch d.Severity {
case analysis.Error:
return 1
case analysis.Warning:
if strict {
return 1
}
}
}
}
return 0
}

// Static checks

var (
_ = Marshaler(jsonMarshaler{})
_ = Marshaler(MarshalCLI)
)
126 changes: 44 additions & 82 deletions pkg/cmd/plugincheck2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,25 @@ import (
"bytes"
"crypto/md5"
"crypto/sha1"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/bmatcuk/doublestar/v4"
"github.com/fatih/color"
"gopkg.in/yaml.v3"

"github.com/grafana/plugin-validator/pkg/analysis"
"github.com/grafana/plugin-validator/pkg/analysis/output"
"github.com/grafana/plugin-validator/pkg/analysis/passes"
"github.com/grafana/plugin-validator/pkg/archivetool"
"github.com/grafana/plugin-validator/pkg/logme"
"github.com/grafana/plugin-validator/pkg/repotool"
"github.com/grafana/plugin-validator/pkg/runner"
)

type FormattedOutput struct {
ID string `json:"id"`
Version string `json:"version"`
Diagnostics map[string][]analysis.Diagnostic `json:"plugin-validator"`
}

func main() {
var (
strictFlag = flag.Bool(
Expand Down Expand Up @@ -174,94 +168,62 @@ func main() {
logme.DebugFln("check failed: %v", err)
}

var exitCode int
var jsonOutput = ""
var outputMarshaler output.Marshaler

// calculate json for either json cli output or file json output
if *outputToFile != "" || cfg.Global.JSONOutput {
pluginID, pluginVersion, err := GetIDAndVersion(archiveDir)
if err != nil {
pluginID, pluginVersion = GetIDAndVersionFallBack(archiveDir)
archiveDiag := analysis.Diagnostic{
Name: "zip-invalid",
Severity: analysis.Error,
Title: "Plugin archive is improperly structured",
Detail: "It is possible your plugin archive structure is incorrect. Please see https://grafana.com/developers/plugin-tools/publish-a-plugin/package-a-plugin for more information on how to package a plugin.",
}
diags["archive"] = append(diags["archive"], archiveDiag)
}
allData := FormattedOutput{
ID: pluginID,
Version: pluginVersion,
Diagnostics: diags,
}
output, err := json.MarshalIndent(allData, "", " ")
if err != nil {
logme.Errorln(fmt.Errorf("couldn't marshal output to json: %w", err))
// Plugin ID and version (needed by JSON output)
pluginID, pluginVersion, err := GetIDAndVersion(archiveDir)
if err != nil {
pluginID, pluginVersion = GetIDAndVersionFallBack(archiveDir)
archiveDiag := analysis.Diagnostic{
Name: "zip-invalid",
Severity: analysis.Error,
Title: "Plugin archive is improperly structured",
Detail: "It is possible your plugin archive structure is incorrect. Please see https://grafana.com/developers/plugin-tools/publish-a-plugin/package-a-plugin for more information on how to package a plugin.",
}
jsonOutput = string(output)
diags["archive"] = append(diags["archive"], archiveDiag)
}

// Additional JSON output to file
if *outputToFile != "" {
if err := os.WriteFile(*outputToFile, []byte(jsonOutput), 0644); err != nil {
ob, err := output.NewJSONMarshaler(pluginID, pluginVersion).Marshal(diags)
if err != nil {
logme.Errorln(fmt.Errorf("couldn't marshal output: %w", err))
os.Exit(1)
}
if err := os.WriteFile(*outputToFile, ob, 0644); err != nil {
logme.Errorln(fmt.Errorf("couldn't write output to file: %w", err))
}
}

// JSON output
// Stdout/Stderr output.

// Determine the correct marshaler depending on the config
if cfg.Global.JSONOutput {
for name := range diags {
for _, d := range diags[name] {
switch d.Severity {
case analysis.Error:
exitCode = 1
case analysis.Warning:
if *strictFlag {
exitCode = 1
}
}
}
}
fmt.Println(jsonOutput)
os.Exit(exitCode)
outputMarshaler = output.NewJSONMarshaler(pluginID, pluginVersion)
} else {
outputMarshaler = output.MarshalCLI
}

// regular CLI output
for name := range diags {
for _, d := range diags[name] {
var buf bytes.Buffer
switch d.Severity {
case analysis.Error:
buf.WriteString(color.RedString("error: "))
exitCode = 1
case analysis.Warning:
buf.WriteString(color.YellowString("warning: "))
if *strictFlag {
exitCode = 1
}
case analysis.Recommendation:
buf.WriteString(color.CyanString("recommendation: "))
case analysis.OK:
buf.WriteString(color.GreenString("ok: "))
case analysis.SuspectedProblem:
buf.WriteString(color.YellowString("suspected: "))
}

if d.Context != "" {
buf.WriteString(d.Context + ": ")
}

buf.WriteString(d.Title)
if len(d.Detail) > 0 {
buf.WriteString("\n" + color.BlueString("detail: "))
buf.WriteString(d.Detail)
}
fmt.Fprintln(os.Stderr, buf.String())
}
// Write to stdout or stderr, depending on config
var outWriter io.Writer
if cfg.Global.JSONOutput {
outWriter = os.Stdout
} else {
outWriter = os.Stderr
}

logme.DebugFln("exit code: %d", exitCode)
os.Exit(exitCode)
// Write output with the correct marshaler, depending on the config, then exit.
// Nothing else should be printed from here on, or the output may become invalid.
ob, err := outputMarshaler.Marshal(diags)
if err != nil {
logme.Errorln(fmt.Errorf("couldn't marshal output: %w", err))
os.Exit(1)
}
if _, err = fmt.Fprintln(outWriter, string(ob)); err != nil {
logme.Errorln(fmt.Errorf("couldn't write output: %w", err))
os.Exit(1)
}
os.Exit(output.ExitCode(*strictFlag, diags))
}

func readConfigFile(path string) (runner.Config, error) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Check(
params analysis.CheckParams,
cfg Config,
severityOverwrite analysis.Severity,
) (map[string][]analysis.Diagnostic, error) {
) (analysis.Diagnostics, error) {
pluginId, err := utils.GetPluginId(params.ArchiveDir)
if err != nil {
// we only need the pluginId to check for exceptions
Expand All @@ -48,7 +48,7 @@ func Check(
}

initAnalyzers(analyzers, &cfg, pluginId, severityOverwrite)
diagnostics := make(map[string][]analysis.Diagnostic)
diagnostics := make(analysis.Diagnostics)

pass := &analysis.Pass{
RootDir: params.ArchiveDir,
Expand Down