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
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func setupManager(log logr.Logger, opts options.Options) (manager.Manager, error

log.Info("Initialize Validation Engine")

err = validations.InitEngine(opts.ConfigFile, metrics)
validationEngine, err := validations.NewValidationEngine(opts.ConfigFile, metrics)
if err != nil {
return nil, fmt.Errorf("initializing validation engine: %w", err)
}
Expand All @@ -140,7 +140,7 @@ func setupManager(log logr.Logger, opts options.Options) (manager.Manager, error
return nil, fmt.Errorf("initializing discovery client: %w", err)
}

gr, err := controller.NewGenericReconciler(mgr.GetClient(), discoveryClient, cmWatcher)
gr, err := controller.NewGenericReconciler(mgr.GetClient(), discoveryClient, cmWatcher, validationEngine)
if err != nil {
return nil, fmt.Errorf("initializing generic reconciler: %w", err)
}
Expand Down
20 changes: 11 additions & 9 deletions pkg/controller/generic_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ type GenericReconciler struct {
discovery discovery.DiscoveryInterface
logger logr.Logger
cmWatcher *configmap.Watcher
validationEngine validations.Interface
}

// NewGenericReconciler returns a GenericReconciler struct
func NewGenericReconciler(
client client.Client,
discovery discovery.DiscoveryInterface,
cmw *configmap.Watcher,
validationEngine validations.Interface,
) (*GenericReconciler, error) {
listLimit, err := getListLimit()
if err != nil {
Expand All @@ -61,6 +63,7 @@ func NewGenericReconciler(
currentObjects: newValidationCache(),
logger: ctrl.Log.WithName("reconcile"),
cmWatcher: cmw,
validationEngine: validationEngine,
}, nil
}

Expand Down Expand Up @@ -125,18 +128,17 @@ func (gr *GenericReconciler) LookForConfigUpdates(ctx context.Context) {
for {
select {
case cfg := <-gr.cmWatcher.ConfigChanged():
validations.UpdateConfig(cfg)
err := validations.InitRegistry()
if err == nil {
gr.objectValidationCache.drain()
validations.ResetMetrics()

} else {
gr.validationEngine.SetConfig(cfg)
err := gr.validationEngine.InitRegistry()
if err != nil {
gr.logger.Error(
err,
fmt.Sprintf("error updating configuration from ConfigMap: %v\n", cfg),
)
continue
}
gr.objectValidationCache.drain()
gr.validationEngine.ResetMetrics()

case <-ctx.Done():
return
Expand Down Expand Up @@ -300,7 +302,7 @@ func (gr *GenericReconciler) reconcileGroupOfObjects(ctx context.Context,
cliObjects = append(cliObjects, typedClientObject)
}

outcome, err := validations.RunValidationsForObjects(cliObjects, namespaceUID)
outcome, err := gr.validationEngine.RunValidationsForObjects(cliObjects, namespaceUID)
if err != nil {
return fmt.Errorf("running validations: %w", err)
}
Expand Down Expand Up @@ -363,7 +365,7 @@ func (gr *GenericReconciler) handleResourceDeletions() {
UID: v.uid,
}

validations.DeleteMetrics(req.ToPromLabels())
gr.validationEngine.DeleteMetrics(req.ToPromLabels())

gr.objectValidationCache.removeKey(k)

Expand Down
8 changes: 7 additions & 1 deletion pkg/controller/generic_reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/app-sre/deployment-validation-operator/pkg/configmap"
"github.com/app-sre/deployment-validation-operator/pkg/validations"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -1029,5 +1030,10 @@ func createTestReconciler(scheme *runtime.Scheme, objects []client.Object) (*Gen
}
client := cliBuilder.Build()
cli := kubefake.NewSimpleClientset()
return NewGenericReconciler(client, cli.Discovery(), &configmap.Watcher{})

ve, err := validations.NewValidationEngine("", make(map[string]*prometheus.GaugeVec))
if err != nil {
return nil, err
}
return NewGenericReconciler(client, cli.Discovery(), &configmap.Watcher{}, ve)
}
141 changes: 0 additions & 141 deletions pkg/validations/base.go
Original file line number Diff line number Diff line change
@@ -1,152 +1,11 @@
package validations

import (
"fmt"
"reflect"

"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"

"github.com/app-sre/deployment-validation-operator/pkg/utils"

"github.com/prometheus/client_golang/prometheus"
"golang.stackrox.io/kube-linter/pkg/lintcontext"
"golang.stackrox.io/kube-linter/pkg/run"
)

var log = logf.Log.WithName("validations")

type ValidationOutcome string

var (
ObjectNeedsImprovement ValidationOutcome = "object needs improvement"
ObjectValid ValidationOutcome = "object valid"
ObjectsValid ValidationOutcome = "objects are valid"
ObjectValidationIgnored ValidationOutcome = "object validation ignored"
)

// RunValidationsForObjects runs validation for the group of related objects
func RunValidationsForObjects(objects []client.Object, namespaceUID string) (ValidationOutcome, error) {
lintCtx := &lintContextImpl{}
for _, obj := range objects {
// Only run checks against an object with no owners. This should be
// the object that controls the configuration
if !utils.IsOwner(obj) {
continue
}
// If controller has no replicas clear do not run any validations
if isControllersWithNoReplicas(obj) {
continue
}
lintCtx.addObjects(lintcontext.Object{K8sObject: obj})
}
lintCtxs := []lintcontext.LintContext{lintCtx}
if len(lintCtxs) == 0 {
return ObjectValidationIgnored, nil
}
result, err := run.Run(lintCtxs, engine.CheckRegistry(), engine.EnabledChecks())
if err != nil {
log.Error(err, "error running validations")
return "", fmt.Errorf("error running validations: %v", err)
}

// Clear labels from past run to ensure only results from this run
// are reflected in the metrics
for _, o := range objects {
req := NewRequestFromObject(o)
req.NamespaceUID = namespaceUID
engine.ClearMetrics(result.Reports, req.ToPromLabels())
}
return processResult(result, namespaceUID)
}

// isControllersWithNoReplicas checks if the provided object has no replicas
func isControllersWithNoReplicas(obj client.Object) bool {
objValue := reflect.Indirect(reflect.ValueOf(obj))
spec := objValue.FieldByName("Spec")
if spec.IsValid() {
replicas := spec.FieldByName("Replicas")
if replicas.IsValid() {
numReplicas, ok := replicas.Interface().(*int32)

// clear labels if we fail to get a value for numReplicas, or if value is <= 0
if !ok || numReplicas == nil || *numReplicas <= 0 {
req := NewRequestFromObject(obj)
engine.DeleteMetrics(req.ToPromLabels())
return true
}
}
}
return false
}

func processResult(result run.Result, namespaceUID string) (ValidationOutcome, error) {
outcome := ObjectValid
for _, report := range result.Reports {
check, err := engine.GetCheckByName(report.Check)
if err != nil {
log.Error(err, fmt.Sprintf("Failed to get check '%s' by name", report.Check))
return "", fmt.Errorf("error running validations: %v", err)
}
obj := report.Object.K8sObject
logger := log.WithValues(
"request.namespace", obj.GetNamespace(),
"request.name", obj.GetName(),
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
"validation", report.Check,
"check_description", check.Description,
"check_remediation", report.Remediation,
"check_failure_reason", report.Diagnostic.Message,
)
metric := engine.GetMetric(report.Check)
if metric == nil {
log.Error(nil, "no metric found for validation", report.Check)
} else {
req := NewRequestFromObject(obj)
req.NamespaceUID = namespaceUID
metric.With(req.ToPromLabels()).Set(1)
logger.Info(report.Remediation)
outcome = ObjectNeedsImprovement
}
}
return outcome, nil
}

// RunValidations will run all the registered validations
func RunValidations(request Request, obj client.Object) (ValidationOutcome, error) {
log.V(2).Info("validation", "kind", request.Kind)

promLabels := request.ToPromLabels()

// Only run checks against an object with no owners. This should be
// the object that controls the configuration
if !utils.IsOwner(obj) {
return ObjectValidationIgnored, nil
}

// If controller has no replicas clear existing metrics and
// do not run any validations
if isControllersWithNoReplicas(obj) {
return ObjectValidationIgnored, nil
}

lintCtxs := []lintcontext.LintContext{}
lintCtx := &lintContextImpl{}
lintCtx.addObjects(lintcontext.Object{K8sObject: obj})
lintCtxs = append(lintCtxs, lintCtx)
result, err := run.Run(lintCtxs, engine.CheckRegistry(), engine.EnabledChecks())
if err != nil {
log.Error(err, "error running validations")
return "", fmt.Errorf("error running validations: %v", err)
}

// Clear labels from past run to ensure only results from this run
// are reflected in the metrics
engine.ClearMetrics(result.Reports, promLabels)

return processResult(result, request.NamespaceUID)
}

// NewRequestFromObject converts a client.Object into
// a validation request. Note that the NamespaceUID of the
// request cannot be derived from the object and should
Expand Down
Loading