Skip to content

Commit

Permalink
api/vmrule: adds validation webhook (#479)
Browse files Browse the repository at this point in the history
it allows to check syntax for VMRules and mitigate possible errors with it vmalert
#471
  • Loading branch information
f41gh7 committed May 17, 2022
1 parent f053efc commit f468ae0
Show file tree
Hide file tree
Showing 15 changed files with 409 additions and 923 deletions.
23 changes: 13 additions & 10 deletions api/go.mod
Expand Up @@ -3,7 +3,7 @@ module github.com/VictoriaMetrics/operator/api
go 1.17

require (
github.com/VictoriaMetrics/VictoriaMetrics v1.71.0
github.com/VictoriaMetrics/VictoriaMetrics v1.77.1
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.23.1
Expand All @@ -14,8 +14,9 @@ require (
)

require (
github.com/VictoriaMetrics/fasthttp v1.1.0 // indirect
github.com/VictoriaMetrics/metrics v1.18.1 // indirect
github.com/VictoriaMetrics/metricsql v0.34.0 // indirect
github.com/VictoriaMetrics/metricsql v0.43.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -25,35 +26,37 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.4 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/common v0.34.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/valyala/histogram v1.2.0 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
github.com/valyala/quicktemplate v1.7.0 // indirect
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/client-go v0.23.1 // indirect
Expand Down
886 changes: 30 additions & 856 deletions api/go.sum

Large diffs are not rendered by default.

32 changes: 20 additions & 12 deletions api/v1beta1/vmrule_types.go
Expand Up @@ -2,7 +2,7 @@ package v1beta1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"net/url"
)

// VMRuleSpec defines the desired state of VMRule
Expand All @@ -18,51 +18,59 @@ type RuleGroup struct {
Name string `json:"name"`
// evaluation interval for group
// +optional
Interval string `json:"interval,omitempty"`
Interval string `json:"interval,omitempty" yaml:"interval,omitempty"`
// Rules list of alert rules
Rules []Rule `json:"rules"`
// Concurrency defines how many rules execute at once.
// +optional
Concurrency int `json:"concurrency,omitempty"`
Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
// Labels optional list of labels added to every rule within a group.
// It has priority over the external labels.
// Labels are commonly used for adding environment
// or tenant-specific tag.
// +optional
Labels map[string]string `json:"labels,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// ExtraFilterLabels optional list of label filters applied to every rule's
// request withing a group. Is compatible only with VM datasource.
// See more details at https://docs.victoriametrics.com#prometheus-querying-api-enhancements
// Deprecated, use params instead
// +optional
ExtraFilterLabels map[string]string `json:"extra_filter_labels,omitempty"`
ExtraFilterLabels map[string]string `json:"extra_filter_labels,omitempty" yaml:"extra_filter_labels,omitempty"`
// Tenant id for group, can be used only with enterprise version of vmalert
// See more details at https://docs.victoriametrics.com/vmalert.html#multitenancy
// +optional
Tenant string `json:"tenant,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
// Params optional HTTP URL parameters added to each rule request
// +optional
Params url.Values `json:"params,omitempty" yaml:"params,omitempty"`
// Type defines datasource type for enterprise version of vmalert
// possible values - prometheus,graphite
// +optional
Type string `json:"type,omitempty" yaml:"type,omitempty"`
}

// Rule describes an alerting or recording rule.
// +k8s:openapi-gen=true
type Rule struct {
// Record represents a query, that will be recorded to dataSource
// +optional
Record string `json:"record,omitempty"`
Record string `json:"record,omitempty" yaml:"record,omitempty"`
// Alert is a name for alert
// +optional
Alert string `json:"alert,omitempty"`
Alert string `json:"alert,omitempty" yaml:"alert,omitempty"`
// Expr is query, that will be evaluated at dataSource
// +optional
Expr intstr.IntOrString `json:"expr"`
Expr string `json:"expr" yaml:"expr"`
// For evaluation interval in time.Duration format
// 30s, 1m, 1h or nanoseconds
// +optional
For string `json:"for,omitempty"`
For string `json:"for,omitempty" yaml:"for,omitempty"`
// Labels will be added to rule configuration
// +optional
Labels map[string]string `json:"labels,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// Annotations will be added to rule configuration
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}

// VMRuleStatus defines the observed state of VMRule
Expand Down
83 changes: 83 additions & 0 deletions api/v1beta1/vmrule_webhook.go
@@ -0,0 +1,83 @@
package v1beta1

import (
"fmt"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime"
"net/url"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sync"
)

var initVMAlertNotifier sync.Once

// log is for logging in this package.
var vmrulelog = logf.Log.WithName("vmrule-resource")

func (r *VMRule) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// +kubebuilder:webhook:verbs=create;update,admissionReviewVersions=v1,sideEffects=none,path=/validate-operator-victoriametrics-com-v1beta1-vmrule,mutating=false,failurePolicy=fail,groups=operator.victoriametrics.com,resources=vmrules,versions=v1beta1,name=vvmrule.kb.io
var _ webhook.Validator = &VMRule{}

func (r *VMRule) sanityCheck() error {
initVMAlertNotifier.Do(func() {
u, _ := url.Parse("https://victoriametrics.com/")
notifier.InitTemplateFunc(u)
})

uniqNames := make(map[string]struct{})
for i := range r.Spec.Groups {
group := &r.Spec.Groups[i]
errContext := fmt.Sprintf("VMRule: %s/%s group: %s", r.Namespace, r.Name, group.Name)
if _, ok := uniqNames[group.Name]; ok {
return fmt.Errorf("duplicate group name: %s", errContext)
}
uniqNames[group.Name] = struct{}{}
groupBytes, err := yaml.Marshal(group)
if err != nil {
return fmt.Errorf("cannot marshal %s, err: %w", errContext, err)
}
var vmalertGroup config.Group
if err := yaml.Unmarshal(groupBytes, &vmalertGroup); err != nil {
return fmt.Errorf("cannot parse vmalert group %s, err: %w, r: \n%s", errContext, err, string(groupBytes))
}
if err := vmalertGroup.Validate(true, true); err != nil {
return fmt.Errorf("validation failed for %s err: %w", errContext, err)
}
}
vmrulelog.Info("successfully validated rule", "name", r.Name)
return nil
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *VMRule) ValidateCreate() error {
vmrulelog.Info("validate create", "name", r.Name)
// skip validation, if object has annotation.
if mustSkipValidation(r) {
return nil
}
return r.sanityCheck()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *VMRule) ValidateUpdate(old runtime.Object) error {
vmrulelog.Info("validate update", "name", r.Name)
if mustSkipValidation(r) {
return nil
}
return r.sanityCheck()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *VMRule) ValidateDelete() error {
// noop
return nil
}
142 changes: 142 additions & 0 deletions api/v1beta1/vmrule_webhook_test.go
@@ -0,0 +1,142 @@
package v1beta1

import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
)

func TestVMRule_sanityCheck(t *testing.T) {
type fields struct {
TypeMeta v1.TypeMeta
ObjectMeta v1.ObjectMeta
Spec VMRuleSpec
Status VMRuleStatus
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "simple check",
fields: fields{
Spec: VMRuleSpec{
Groups: []RuleGroup{
{
Name: "group name",
Rules: []Rule{
{
Alert: "hosts down",
Expr: "up == 0",
},
},
},
},
},
},
},
{
name: "duplicate groups",
fields: fields{
Spec: VMRuleSpec{
Groups: []RuleGroup{
{
Name: "group name",
Rules: []Rule{
{
Alert: "hosts down",
Expr: "up == 0",
},
},
},
{
Name: "group name",
Rules: []Rule{
{
Alert: "hosts down",
Expr: "up == 0",
},
},
},
},
},
},
wantErr: true,
},
{
name: "incorrect expr",
fields: fields{
Spec: VMRuleSpec{
Groups: []RuleGroup{
{
Name: "group name",
Rules: []Rule{
{
Alert: "hosts down",
Expr: "asf124qaf(fa!@$qrfz",
},
},
},
},
},
},
wantErr: true,
},
{
name: "incorrect annotation",
fields: fields{
Spec: VMRuleSpec{
Groups: []RuleGroup{
{
Name: "group name",
Rules: []Rule{
{
Alert: "hosts down",
Expr: "asf124qaf()",
Annotations: map[string]string{
"name": "{{$BadSyntax}}",
},
},
},
},
},
},
},
wantErr: true,
},
{
name: "correct annotation with query",
fields: fields{
Spec: VMRuleSpec{
Groups: []RuleGroup{
{
Name: "group name",
Rules: []Rule{
{
Alert: "hosts down",
Expr: "down == 1",
Annotations: map[string]string{
"name": "{{ range printf \"alertmanager_config_hash{namespace=\\\"%s\\\",service=\\\"%s\\\"}\" $labels.namespace $labels.service | query }}\n Configuration hash for pod {{ .Labels.pod }} is \"{{ printf \"%.f\" .Value }}\"\n {{ end }}",
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &VMRule{
TypeMeta: tt.fields.TypeMeta,
ObjectMeta: tt.fields.ObjectMeta,
Spec: tt.fields.Spec,
Status: tt.fields.Status,
}
if err := r.sanityCheck(); (err != nil) != tt.wantErr {
t.Errorf("unexpected error: %v", err)
}
})
}
}
1 change: 0 additions & 1 deletion api/v1beta1/vmsingle_webhook.go
Expand Up @@ -52,7 +52,6 @@ func (r *VMSingle) ValidateUpdate(old runtime.Object) error {

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *VMSingle) ValidateDelete() error {

// no-op
return nil
}

0 comments on commit f468ae0

Please sign in to comment.