[Provisioning] Remove linting from MVP (#101286)

* Remove Linting backend

* Re-generate client
This commit is contained in:
Roberto Jiménez Sánchez authored and GitHub committed 2025-02-25 12:47:10 +01:00
1 parent b7b6e58cfe
commit caf4b3d346
12 files changed
+4 -431

No files matched your search

-23
View File
@@ -231,34 +231,11 @@ type ResourceWrapper struct {
// Different flavors of the same object
Resource ResourceObjects `json:"resource"`
// Lint results
// +listType=atomic
Lint []LintIssue `json:"lint,omitempty"`
// If errors exist, show them here
// +listType=atomic
Errors []string `json:"errors,omitempty"`
}
// The kubernetes action required when loading a given resource
// +enum
type LintSeverity string
// ResourceAction values
const (
LintSeverityExclude LintSeverity = "exclude"
LintSeverityQuiet LintSeverity = "quiet"
LintSeverityWarning LintSeverity = "warning"
LintSeverityError LintSeverity = "error"
LintSeverityFixed LintSeverity = "fixed"
)
type LintIssue struct {
Severity LintSeverity `json:"severity"`
Rule string `json:"rule"`
Message string `json:"message"`
}
type ResourceType struct {
Group string `json:"group,omitempty"`
Version string `json:"version,omitempty"`
@@ -336,22 +336,6 @@ func (in *JobStatus) DeepCopy() *JobStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LintIssue) DeepCopyInto(out *LintIssue) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LintIssue.
func (in *LintIssue) DeepCopy() *LintIssue {
if in == nil {
return nil
}
out := new(LintIssue)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalRepositoryConfig) DeepCopyInto(out *LocalRepositoryConfig) {
*out = *in
@@ -706,11 +690,6 @@ func (in *ResourceWrapper) DeepCopyInto(out *ResourceWrapper) {
*out = (*in).DeepCopy()
}
in.Resource.DeepCopyInto(&out.Resource)
if in.Lint != nil {
in, out := &in.Lint, &out.Lint
*out = make([]LintIssue, len(*in))
copy(*out, *in)
}
if in.Errors != nil {
in, out := &in.Errors, &out.Errors
*out = make([]string, len(*in))
@@ -27,7 +27,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobResourceSummary": schema_pkg_apis_provisioning_v0alpha1_JobResourceSummary(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobSpec": schema_pkg_apis_provisioning_v0alpha1_JobSpec(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobStatus": schema_pkg_apis_provisioning_v0alpha1_JobStatus(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LintIssue": schema_pkg_apis_provisioning_v0alpha1_LintIssue(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_LocalRepositoryConfig(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.MigrateJobOptions": schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.PullRequestJobOptions": schema_pkg_apis_provisioning_v0alpha1_PullRequestJobOptions(ref),
@@ -742,42 +741,6 @@ func schema_pkg_apis_provisioning_v0alpha1_JobStatus(ref common.ReferenceCallbac
}
}
func schema_pkg_apis_provisioning_v0alpha1_LintIssue(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"severity": {
SchemaProps: spec.SchemaProps{
Description: "Possible enum values:\n - `\"error\"`\n - `\"exclude\"`\n - `\"fixed\"`\n - `\"quiet\"`\n - `\"warning\"`",
Default: "",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"error", "exclude", "fixed", "quiet", "warning"},
},
},
"rule": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"message": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"severity", "rule", "message"},
},
},
}
}
func schema_pkg_apis_provisioning_v0alpha1_LocalRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -1576,25 +1539,6 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceC
Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects"),
},
},
"lint": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "Lint results",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LintIssue"),
},
},
},
},
},
"errors": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
@@ -1620,7 +1564,7 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceC
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LintIssue", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
}
}
@@ -1,54 +0,0 @@
package pullrequest
import (
"bytes"
"context"
"fmt"
"html/template"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
type Linter struct {
lintTemplate *template.Template
available bool
}
func NewLinter(available bool) *Linter {
return &Linter{
lintTemplate: template.Must(template.New("lint").Parse(lintDashboardIssuesTemplate)),
available: available,
}
}
func (l *Linter) IsAvailable() bool {
return l.available
}
const lintDashboardIssuesTemplate = `Hey there! 👋
Grafana found some linting issues in this dashboard you may want to check:
{{ range .}}
{{ if eq .Severity "error" }}❌{{ else if eq .Severity "warning" }}⚠️ {{ end }} [dashboard-linter/{{ .Rule }}](https://github.com/grafana/dashboard-linter/blob/main/docs/rules/{{ .Rule }}.md): {{ .Message }}.
{{- end }}`
func (l *Linter) Lint(
ctx context.Context,
prRepo PullRequestRepo,
options *provisioning.PullRequestJobOptions,
path string,
ref string,
lintResults []provisioning.LintIssue,
) error {
var buf bytes.Buffer
if err := l.lintTemplate.Execute(&buf, lintResults); err != nil {
return fmt.Errorf("execute lint comment template: %w", err)
}
if err := prRepo.CommentPullRequestFile(ctx, options.PR, path, ref, buf.String()); err != nil {
return fmt.Errorf("comment pull request file %s: %w", path, err)
}
logging.FromContext(ctx).Info("lint comment added", "path", path)
return nil
}
@@ -25,18 +25,15 @@ type PullRequestRepo interface {
type PullRequestWorker struct {
parsers *resources.ParserFactory
linter *Linter
previewer *Previewer
}
func NewPullRequestWorker(
parsers *resources.ParserFactory,
previewer *Previewer,
linter *Linter,
) (*PullRequestWorker, error) {
return &PullRequestWorker{
parsers: parsers,
linter: linter,
previewer: previewer,
}, nil
}
@@ -130,14 +127,6 @@ func (c *PullRequestWorker) Process(ctx context.Context,
result.Group = parsed.GVR.Group
result.Name = parsed.Obj.GetName()
if f.Action != repository.FileActionDeleted && len(parsed.Lint) > 0 {
if err := c.linter.Lint(ctx, prRepo, options, f.Path, ref, parsed.Lint); err != nil {
result.Error = fmt.Errorf("failed to lint file: %w", err)
progress.Record(ctx, result)
continue
}
}
preview, err := c.previewer.Preview(ctx, f, job.Namespace, repo.Config().Name, cfg.GitHub.Branch, ref, options.URL, cfg.GitHub.GenerateDashboardPreviews)
if err != nil {
result.Error = fmt.Errorf("create preview: %w", err)
@@ -1,119 +0,0 @@
package lint
import (
"context"
"encoding/json"
"fmt"
"sort"
"github.com/grafana/dashboard-linter/lint"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"gopkg.in/yaml.v3"
)
type DashboardLinterFactory struct {
enabled bool
}
func NewDashboardLinterFactory(enabled bool) *DashboardLinterFactory {
return &DashboardLinterFactory{enabled: enabled}
}
func (f *DashboardLinterFactory) New() Linter {
return &DashboardLinter{rules: lint.NewRuleSet()}
}
func (f *DashboardLinterFactory) NewFromConfig(cfg []byte) (Linter, error) {
var lintCfg lint.ConfigurationFile
if err := yaml.Unmarshal(cfg, &lintCfg); err != nil {
return nil, fmt.Errorf("unmarshal linter config: %w", err)
}
return &DashboardLinter{rules: lint.NewRuleSet(), cfg: lintCfg}, nil
}
func (f *DashboardLinterFactory) IsEnabled() bool {
return f.enabled
}
func (f *DashboardLinterFactory) ConfigPath() string {
return ".dashboard-lint"
}
type DashboardLinter struct {
rules lint.RuleSet
cfg lint.ConfigurationFile
}
// FIXME: what would be a good place to put all the schema validation?
type specData struct {
Spec json.RawMessage `json:"spec"`
}
func NewDashboardLinter() *DashboardLinter {
// TODO: read rules from configuration and pass to the lint function
return &DashboardLinter{rules: lint.NewRuleSet()}
}
func (l *DashboardLinter) Lint(ctx context.Context, buf []byte) ([]provisioning.LintIssue, error) {
var data specData
// Try to parse the spec from data
if err := json.Unmarshal(buf, &data); err == nil && len(data.Spec) > 0 {
buf = data.Spec
}
dashboard, err := lint.NewDashboard(buf)
if err != nil {
return nil, fmt.Errorf("failed to parse dashboard with linter: %v", err)
}
results, err := l.rules.Lint([]lint.Dashboard{dashboard})
if err != nil {
return nil, fmt.Errorf("failed to lint dashboard: %v", err)
}
results.Configure(&l.cfg)
byRule := results.ByRule()
rules := make([]string, 0, len(byRule))
for r := range byRule {
rules = append(rules, r)
}
sort.Strings(rules)
issues := make([]provisioning.LintIssue, 0)
for _, rule := range rules {
for _, rr := range byRule[rule] {
for _, r := range rr.Result.Results {
if r.Severity != lint.Error && r.Severity != lint.Warning {
continue
}
issues = append(issues, provisioning.LintIssue{
Rule: rule,
Severity: toLintSeverity(r.Severity),
Message: r.Message,
})
}
}
}
return issues, nil
}
func toLintSeverity(s lint.Severity) provisioning.LintSeverity {
switch s {
case lint.Error:
return provisioning.LintSeverityError
case lint.Exclude:
return provisioning.LintSeverityExclude
case lint.Fixed:
return provisioning.LintSeverityFixed
case lint.Quiet:
return provisioning.LintSeverityQuiet
case lint.Warning:
return provisioning.LintSeverityWarning
default:
}
return "" // unknown
}
@@ -1,18 +0,0 @@
package lint
import (
"context"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
type Linter interface {
Lint(ctx context.Context, data []byte) ([]provisioning.LintIssue, error)
}
type LinterFactory interface {
ConfigPath() string
IsEnabled() bool
NewFromConfig(cfg []byte) (Linter, error)
New() Linter
}
+2 -9
View File
@@ -41,7 +41,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/migrate"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/pullrequest"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/lint"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
@@ -80,7 +79,6 @@ type APIBuilder struct {
blobstore blob.PublicBlobStore
client *resources.ClientFactory
parsers *resources.ParserFactory
linters lint.LinterFactory
ghFactory *github.Factory
clonedir string // where repo clones are managed
jobs jobs.JobQueue
@@ -112,8 +110,6 @@ func NewAPIBuilder(
secrets secrets.Service,
) *APIBuilder {
clientFactory := resources.NewFactory(configProvider)
// TODO: linting is experimental, so we disable it for now
linters := lint.NewDashboardLinterFactory(false)
return &APIBuilder{
urlProvider: urlProvider,
localFileResolver: local,
@@ -121,10 +117,8 @@ func NewAPIBuilder(
features: features,
ghFactory: ghFactory,
client: clientFactory,
linters: linters,
parsers: &resources.ParserFactory{
Client: clientFactory,
LinterFactory: linters,
Client: clientFactory,
},
render: render,
clonedir: clonedir,
@@ -431,10 +425,9 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH
))
// Pull request worker
linter := pullrequest.NewLinter(b.linters.IsEnabled())
renderer := pullrequest.NewScreenshotRenderer(b.render, b.blobstore)
previewer := pullrequest.NewPreviewer(renderer, b.urlProvider)
pullRequestWorker, err := pullrequest.NewPullRequestWorker(b.parsers, previewer, linter)
pullRequestWorker, err := pullrequest.NewPullRequestWorker(b.parsers, previewer)
if err != nil {
return fmt.Errorf("create pull request worker: %w", err)
}
@@ -3,7 +3,6 @@ package resources
import (
"bytes"
"context"
"encoding/json"
"os"
"path"
"testing"
@@ -13,7 +12,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/lint"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
@@ -110,7 +108,6 @@ spec:
// try to validate (and lint)
validate := true
parser.linter = lint.NewDashboardLinter()
// Support dashboard conversion
parsed, err := parser.Parse(context.Background(), info, validate)
@@ -122,21 +119,5 @@ spec:
Version: "v1alpha1",
Kind: "Dashboard",
}, parsed.GVK)
jj, err := json.MarshalIndent(parsed.Lint, "", " ")
require.NoError(t, err)
// fmt.Printf("%s\n", string(jj))
require.JSONEq(t, `[
{
"severity": "error",
"rule": "template-datasource-rule",
"message": "Dashboard 'Timeline Demo' does not have a templated data source"
},
{
"severity": "error",
"rule": "uneditable-dashboard",
"message": "Dashboard 'Timeline Demo' is editable, it should be set to 'editable: false'"
}
]`, string(jj))
})
}
@@ -9,7 +9,6 @@ import (
"path"
"gopkg.in/yaml.v3"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -20,15 +19,13 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/dashboard"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/lint"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
var ErrNamespaceMismatch = errors.New("the file namespace does not match target namespace")
type ParserFactory struct {
Client *ClientFactory
LinterFactory lint.LinterFactory
Client *ClientFactory
}
func (f *ParserFactory) GetParser(ctx context.Context, repo repository.Reader) (*Parser, error) {
@@ -44,27 +41,6 @@ func (f *ParserFactory) GetParser(ctx context.Context, repo repository.Reader) (
kinds: kinds,
}
if f.LinterFactory.IsEnabled() {
cfg, err := repo.Read(ctx, f.LinterFactory.ConfigPath(), "")
logger := logging.FromContext(ctx)
var linter lint.Linter
switch {
case err == nil:
logger.Info("linter config found", "config", string(cfg.Data))
linter, err = f.LinterFactory.NewFromConfig(cfg.Data)
if err != nil {
return nil, fmt.Errorf("failed to create linter: %w", err)
}
case apierrors.IsNotFound(err):
logger.Info("no linter config found, using default")
linter = f.LinterFactory.New()
default:
return nil, fmt.Errorf("failed to read linter config: %w", err)
}
parser.linter = linter
}
return parser, nil
}
@@ -75,7 +51,6 @@ type Parser struct {
// client helper (for this namespace?)
client *DynamicClient
kinds KindsLookup
linter lint.Linter
}
type ParsedResource struct {
@@ -107,9 +82,6 @@ type ParsedResource struct {
// The results from dry run
DryRunResponse *unstructured.Unstructured
// Optional lint issues
Lint []provisioning.LintIssue
// If we got some Errors
Errors []error
}
@@ -192,20 +164,6 @@ func (r *Parser) Parse(ctx context.Context, info *repository.FileInfo, validate
return parsed, nil
}
if r.linter != nil { // lint
raw := info.Data
if parsed.Classic == provisioning.ClassicDashboard {
raw, err = json.MarshalIndent(parsed.Obj, "", " ") // indent so it is not all on one line
if err != nil {
return parsed, err
}
}
parsed.Lint, err = r.linter.Lint(ctx, raw)
if err != nil {
parsed.Errors = append(parsed.Errors, err)
}
}
if parsed.Client == nil {
parsed.Errors = append(parsed.Errors, fmt.Errorf("unable to find client"))
return parsed, nil
@@ -288,7 +246,6 @@ func (f *ParsedResource) AsResourceWrapper() *provisioning.ResourceWrapper {
Hash: info.Hash,
Timestamp: info.Modified,
Resource: res,
Lint: f.Lint,
}
for _, err := range f.Errors {
wrap.Errors = append(wrap.Errors, err.Error())
@@ -3209,36 +3209,6 @@
}
}
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.LintIssue": {
"type": "object",
"required": [
"severity",
"rule",
"message"
],
"properties": {
"message": {
"type": "string",
"default": ""
},
"rule": {
"type": "string",
"default": ""
},
"severity": {
"description": "Possible enum values:\n - `\"error\"`\n - `\"exclude\"`\n - `\"fixed\"`\n - `\"quiet\"`\n - `\"warning\"`",
"type": "string",
"default": "",
"enum": [
"error",
"exclude",
"fixed",
"quiet",
"warning"
]
}
}
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.LocalRepositoryConfig": {
"type": "object",
"properties": {
@@ -3832,19 +3802,6 @@
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"lint": {
"description": "Lint results",
"type": "array",
"items": {
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.LintIssue"
}
]
},
"x-kubernetes-list-type": "atomic"
},
"path": {
"description": "Path to the remote file",
"type": "string"
@@ -1023,17 +1023,6 @@ export type DeleteOptions = {
/** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */
propagationPolicy?: string;
};
export type LintIssue = {
message: string;
rule: string;
/** Possible enum values:
- `"error"`
- `"exclude"`
- `"fixed"`
- `"quiet"`
- `"warning"` */
severity: 'error' | 'exclude' | 'fixed' | 'quiet' | 'warning';
};
export type Unstructured = {
[key: string]: any;
};
@@ -1077,8 +1066,6 @@ export type ResourceWrapper = {
hash?: string;
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
/** Lint results */
lint?: LintIssue[];
/** Path to the remote file */
path?: string;
/** The commit hash (if exists) */