Skip to content

Commit a9c60cf

Browse files
authored
DVO-111: Dynamic validation engine (#263)
* Add filter for other ConfigMaps within the same namespace * Add validationEngine and watcher to reconcilier * Add initializer and accessible function from struct * Fix Manager setup to account for validationEngine and watcher * Fix missing channel * Decouple Prometheus registry initialization * Fix missing interface change * Add new utils functions for Prometheus registry decoupling * Fix global variable missleading * Refactor validation engine package WIP * Refactor re-use existing methods on VE * Refactor minor changes on initializacion * WIP Move configmap watcher to its own package * Refactor main script to improve readability * Fix redundant system trigger of the informer * Rollback VE and CMW dependencies and interface * Move CMW control to VE package * Update comments * Fix linting recommendations * WIP Refactor metrics used on Prom registry to be used in VE struct * Update documentation * Fix loglines prefixes * Fix linter issues * Fix missing prefix on metrics * Fix unit tests breaking due to metrics preload * Fix nit recommendations * Add dynamic namespace setting on watcher * Fix unit tests * Use Pod instead of deployment for namespace * Move back Watcher to reconcilier * Add accesses for reconcilier * Remove Watcher from VE and fix main script * Refactor loglines and pod namespace gather function * Add new logic for removing unexistent checks from configuration * Minor refactoring * Add missing documentation and fix tests * Unexport validation engine structure as now it's not handled outside of the package * Add missing operator.yaml fix to get new namespace ENV variable * Add delete scenario in the watcher * Minor refactoring * Bundle regenerated with missing env variable
1 parent 6d9c194 commit a9c60cf

15 files changed

+653
-235
lines changed

Diff for: bundle.Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/
66
LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/
77
LABEL operators.operatorframework.io.bundle.package.v1=deployment-validation-operator
88
LABEL operators.operatorframework.io.bundle.channels.v1=alpha
9-
LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.28.1
9+
LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.31.0+git
1010
LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1
1111
LABEL operators.operatorframework.io.metrics.project_layout=unknown
1212

Diff for: bundle/manifests/deployment-validation-operator.clusterserviceversion.yaml

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ metadata:
44
annotations:
55
alm-examples: '[]'
66
capabilities: Basic Install
7-
createdAt: "2023-08-24T07:58:38Z"
8-
operators.operatorframework.io/builder: operator-sdk-v1.28.1
7+
createdAt: "2023-09-12T14:13:09Z"
8+
operators.operatorframework.io/builder: operator-sdk-v1.31.0+git
99
operators.operatorframework.io/project_layout: unknown
1010
name: deployment-validation-operator.v0.0.0
1111
namespace: placeholder
@@ -82,6 +82,10 @@ spec:
8282
valueFrom:
8383
fieldRef:
8484
fieldPath: metadata.name
85+
- name: POD_NAMESPACE
86+
valueFrom:
87+
fieldRef:
88+
fieldPath: metadata.namespace
8589
image: quay.io/deployment-validation-operator/dv-operator:latest
8690
imagePullPolicy: Always
8791
livenessProbe:

Diff for: bundle/metadata/annotations.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ annotations:
55
operators.operatorframework.io.bundle.metadata.v1: metadata/
66
operators.operatorframework.io.bundle.package.v1: deployment-validation-operator
77
operators.operatorframework.io.bundle.channels.v1: alpha
8-
operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.1
8+
operators.operatorframework.io.metrics.builder: operator-sdk-v1.31.0+git
99
operators.operatorframework.io.metrics.mediatype.v1: metrics+v1
1010
operators.operatorframework.io.metrics.project_layout: unknown

Diff for: deploy/openshift/operator.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ spec:
8686
valueFrom:
8787
fieldRef:
8888
fieldPath: metadata.name
89+
- name: POD_NAMESPACE
90+
valueFrom:
91+
fieldRef:
92+
fieldPath: metadata.namespace
8993
volumeMounts:
9094
- name: dvo-config
9195
mountPath: /config

Diff for: main.go

+65-36
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import (
1515
apis "github.com/app-sre/deployment-validation-operator/api"
1616
dvconfig "github.com/app-sre/deployment-validation-operator/config"
1717
"github.com/app-sre/deployment-validation-operator/internal/options"
18+
"github.com/app-sre/deployment-validation-operator/pkg/configmap"
1819
"github.com/app-sre/deployment-validation-operator/pkg/controller"
19-
dvo_prom "github.com/app-sre/deployment-validation-operator/pkg/prometheus"
20+
dvoProm "github.com/app-sre/deployment-validation-operator/pkg/prometheus"
2021
"github.com/app-sre/deployment-validation-operator/pkg/validations"
2122
"github.com/app-sre/deployment-validation-operator/version"
2223
"github.com/prometheus/client_golang/prometheus"
@@ -86,68 +87,66 @@ func setupManager(log logr.Logger, opts options.Options) (manager.Manager, error
8687
return nil, fmt.Errorf("getting config: %w", err)
8788
}
8889

89-
log.Info("Initialize Scheme")
90+
log.Info("Initialize Manager")
9091

91-
scheme, err := initializeScheme()
92+
mgr, err := initManager(log, opts, cfg)
9293
if err != nil {
93-
return nil, fmt.Errorf("initializing scheme: %w", err)
94+
return nil, fmt.Errorf("initializing manager: %w", err)
9495
}
9596

96-
log.Info("Initialize Manager")
97+
log.Info("Registering Components")
9798

98-
mgrOpts, err := getManagerOptions(scheme, opts)
99-
if err != nil {
100-
return nil, fmt.Errorf("getting manager options: %w", err)
101-
}
99+
log.Info("Initialize Prometheus Registry")
102100

103-
mgr, err := manager.New(cfg, mgrOpts)
101+
reg := prometheus.NewRegistry()
102+
metrics, err := dvoProm.PreloadMetrics(reg)
104103
if err != nil {
105-
return nil, fmt.Errorf("initializing manager: %w", err)
104+
return nil, fmt.Errorf("preloading kube-linter metrics: %w", err)
106105
}
107106

108-
if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {
109-
return nil, fmt.Errorf("adding healthz check: %w", err)
110-
}
107+
log.Info(fmt.Sprintf("Initialize Prometheus metrics endpoint on %q", opts.MetricsEndpoint()))
111108

112-
if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil {
113-
return nil, fmt.Errorf("adding readyz check: %w", err)
109+
srv, err := dvoProm.NewServer(reg, opts.MetricsPath, fmt.Sprintf(":%d", opts.MetricsPort))
110+
if err != nil {
111+
return nil, fmt.Errorf("initializing metrics server: %w", err)
114112
}
115113

116-
log.Info("Registering Components")
117-
118-
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
119-
if err != nil {
120-
return nil, fmt.Errorf("initializing discovery client: %w", err)
114+
if err := mgr.Add(srv); err != nil {
115+
return nil, fmt.Errorf("adding metrics server to manager: %w", err)
121116
}
122117

123-
gr, err := controller.NewGenericReconciler(mgr.GetClient(), discoveryClient)
118+
log.Info("Initialize ConfigMap watcher")
119+
120+
cmWatcher, err := configmap.NewWatcher(cfg)
124121
if err != nil {
125-
return nil, fmt.Errorf("initializing generic reconciler: %w", err)
122+
return nil, fmt.Errorf("initializing configmap watcher: %w", err)
126123
}
127124

128-
if err = gr.AddToManager(mgr); err != nil {
129-
return nil, fmt.Errorf("adding generic reconciler to manager: %w", err)
125+
if err := mgr.Add(cmWatcher); err != nil {
126+
return nil, fmt.Errorf("adding configmap watcher to manager: %w", err)
130127
}
131128

132-
log.Info("Initializing Prometheus Registry")
129+
log.Info("Initialize Validation Engine")
133130

134-
reg := prometheus.NewRegistry()
131+
err = validations.InitEngine(opts.ConfigFile, metrics)
132+
if err != nil {
133+
return nil, fmt.Errorf("initializing validation engine: %w", err)
134+
}
135135

136-
log.Info(fmt.Sprintf("Initializing Prometheus metrics endpoint on %q", opts.MetricsEndpoint()))
136+
log.Info("Initialize Reconciler")
137137

138-
srv, err := dvo_prom.NewServer(reg, opts.MetricsPath, fmt.Sprintf(":%d", opts.MetricsPort))
138+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
139139
if err != nil {
140-
return nil, fmt.Errorf("initializing metrics server: %w", err)
140+
return nil, fmt.Errorf("initializing discovery client: %w", err)
141141
}
142142

143-
if err := mgr.Add(srv); err != nil {
144-
return nil, fmt.Errorf("adding metrics server to manager: %w", err)
143+
gr, err := controller.NewGenericReconciler(mgr.GetClient(), discoveryClient, cmWatcher)
144+
if err != nil {
145+
return nil, fmt.Errorf("initializing generic reconciler: %w", err)
145146
}
146147

147-
log.Info("Initializing Validation Engine")
148-
149-
if err := validations.InitializeValidationEngine(opts.ConfigFile, reg); err != nil {
150-
return nil, fmt.Errorf("initializing validation engine: %w", err)
148+
if err = gr.AddToManager(mgr); err != nil {
149+
return nil, fmt.Errorf("adding generic reconciler to manager: %w", err)
151150
}
152151

153152
return mgr, nil
@@ -235,3 +234,33 @@ func kubeClientQPS() (float32, error) {
235234
qps = float32(val)
236235
return qps, err
237236
}
237+
238+
func initManager(log logr.Logger, opts options.Options, cfg *rest.Config) (manager.Manager, error) {
239+
log.Info("Initialize Scheme")
240+
scheme, err := initializeScheme()
241+
if err != nil {
242+
return nil, fmt.Errorf("initializing scheme: %w", err)
243+
}
244+
245+
log.Info("Getting Manager Options")
246+
mgrOpts, err := getManagerOptions(scheme, opts)
247+
if err != nil {
248+
return nil, fmt.Errorf("getting manager options: %w", err)
249+
}
250+
251+
mgr, err := manager.New(cfg, mgrOpts)
252+
if err != nil {
253+
return nil, fmt.Errorf("getting new manager: %w", err)
254+
}
255+
256+
log.Info("Adding Healthz and Readyz checks")
257+
if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {
258+
return nil, fmt.Errorf("adding healthz check: %w", err)
259+
}
260+
261+
if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil {
262+
return nil, fmt.Errorf("adding readyz check: %w", err)
263+
}
264+
265+
return mgr, nil
266+
}

Diff for: pkg/configmap/configmap_watcher.go

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package configmap
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"reflect"
8+
"time"
9+
10+
"golang.stackrox.io/kube-linter/pkg/config"
11+
"gopkg.in/yaml.v3"
12+
13+
"github.com/app-sre/deployment-validation-operator/pkg/validations"
14+
"github.com/go-logr/logr"
15+
apicorev1 "k8s.io/api/core/v1"
16+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/client-go/informers"
18+
"k8s.io/client-go/kubernetes"
19+
"k8s.io/client-go/rest"
20+
"k8s.io/client-go/tools/cache"
21+
"sigs.k8s.io/controller-runtime/pkg/log"
22+
)
23+
24+
// this structure mirrors Kube-Linter configuration structure
25+
// it is used as a bridge to unmarshall ConfigMap data
26+
// doc: https://pkg.go.dev/golang.stackrox.io/kube-linter/pkg/config#Config
27+
type KubeLinterChecks struct {
28+
Checks struct {
29+
AddAllBuiltIn bool `yaml:"addAllBuiltIn,omitempty"`
30+
DoNotAutoAddDefaults bool `yaml:"doNotAutoAddDefaults,omitempty"`
31+
Exclude []string `yaml:"exclude,omitempty"`
32+
Include []string `yaml:"include,omitempty"`
33+
IgnorePaths []string `yaml:"ignorePaths,omitempty"`
34+
} `yaml:"checks"`
35+
}
36+
37+
type Watcher struct {
38+
clientset kubernetes.Interface
39+
checks KubeLinterChecks
40+
ch chan config.Config
41+
logger logr.Logger
42+
namespace string
43+
}
44+
45+
var configMapName = "deployment-validation-operator-config"
46+
var configMapDataAccess = "deployment-validation-operator-config.yaml"
47+
48+
// NewWatcher creates a new Watcher instance for observing changes to a ConfigMap.
49+
//
50+
// Parameters:
51+
// - cfg: A pointer to a rest.Config representing the Kubernetes client configuration.
52+
//
53+
// Returns:
54+
// - A pointer to a Watcher instance for monitoring changes to DVO ConfigMap resource.
55+
// - An error if there's an issue while initializing the Kubernetes clientset.
56+
func NewWatcher(cfg *rest.Config) (*Watcher, error) {
57+
clientset, err := kubernetes.NewForConfig(cfg)
58+
if err != nil {
59+
return nil, fmt.Errorf("initializing clientset: %w", err)
60+
}
61+
62+
// the Informer will use this to monitor the namespace for the ConfigMap.
63+
namespace, err := getPodNamespace()
64+
if err != nil {
65+
return nil, fmt.Errorf("getting namespace: %w", err)
66+
}
67+
68+
return &Watcher{
69+
clientset: clientset,
70+
logger: log.Log.WithName("ConfigMapWatcher"),
71+
ch: make(chan config.Config),
72+
namespace: namespace,
73+
}, nil
74+
}
75+
76+
// GetStaticKubelinterConfig returns the ConfigMap's checks configuration
77+
func (cmw *Watcher) GetStaticKubelinterConfig(ctx context.Context) (config.Config, error) {
78+
cm, err := cmw.clientset.CoreV1().
79+
ConfigMaps(cmw.namespace).Get(ctx, configMapName, v1.GetOptions{})
80+
if err != nil {
81+
return config.Config{}, fmt.Errorf("getting initial configuration: %w", err)
82+
}
83+
84+
return cmw.getKubeLinterConfig(cm.Data[configMapDataAccess])
85+
}
86+
87+
// Start will update the channel structure with new configuration data from ConfigMap update event
88+
func (cmw Watcher) Start(ctx context.Context) error {
89+
factory := informers.NewSharedInformerFactoryWithOptions(
90+
cmw.clientset, time.Second*30, informers.WithNamespace(cmw.namespace),
91+
)
92+
informer := factory.Core().V1().ConfigMaps().Informer()
93+
94+
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ // nolint:errcheck
95+
AddFunc: func(obj interface{}) {
96+
newCm := obj.(*apicorev1.ConfigMap)
97+
98+
if configMapName != newCm.GetName() {
99+
return
100+
}
101+
102+
cmw.logger.Info(
103+
"a ConfigMap has been created under watched namespace",
104+
"name", newCm.GetName(),
105+
"namespace", newCm.GetNamespace(),
106+
)
107+
108+
cfg, err := cmw.getKubeLinterConfig(newCm.Data[configMapDataAccess])
109+
if err != nil {
110+
cmw.logger.Error(err, "ConfigMap data format")
111+
return
112+
}
113+
114+
cmw.ch <- cfg
115+
},
116+
UpdateFunc: func(oldObj, newObj interface{}) {
117+
newCm := newObj.(*apicorev1.ConfigMap)
118+
119+
// This is sometimes triggered even if no change was due to the ConfigMap
120+
if configMapName != newCm.GetName() || reflect.DeepEqual(oldObj, newObj) {
121+
return
122+
}
123+
124+
cmw.logger.Info(
125+
"a ConfigMap has been updated under watched namespace",
126+
"name", newCm.GetName(),
127+
"namespace", newCm.GetNamespace(),
128+
)
129+
130+
cfg, err := cmw.getKubeLinterConfig(newCm.Data[configMapDataAccess])
131+
if err != nil {
132+
cmw.logger.Error(err, "ConfigMap data format")
133+
return
134+
}
135+
136+
cmw.ch <- cfg
137+
},
138+
DeleteFunc: func(oldObj interface{}) {
139+
cm := oldObj.(*apicorev1.ConfigMap)
140+
141+
cmw.logger.Info(
142+
"a ConfigMap has been deleted under watched namespace",
143+
"name", cm.GetName(),
144+
"namespace", cm.GetNamespace(),
145+
)
146+
147+
cmw.ch <- config.Config{
148+
Checks: validations.GetDefaultChecks(),
149+
}
150+
},
151+
})
152+
153+
factory.Start(ctx.Done())
154+
155+
return nil
156+
}
157+
158+
// ConfigChanged receives push notifications when the configuration is updated
159+
func (cmw *Watcher) ConfigChanged() <-chan config.Config {
160+
return cmw.ch
161+
}
162+
163+
// getKubeLinterConfig returns a valid Kube-linter Config structure
164+
// based on the checks received by the string
165+
func (cmw *Watcher) getKubeLinterConfig(data string) (config.Config, error) {
166+
var cfg config.Config
167+
168+
err := yaml.Unmarshal([]byte(data), &cmw.checks)
169+
if err != nil {
170+
return cfg, fmt.Errorf("unmarshalling configmap data: %w", err)
171+
}
172+
173+
cfg.Checks = config.ChecksConfig(cmw.checks.Checks)
174+
175+
return cfg, nil
176+
}
177+
178+
func getPodNamespace() (string, error) {
179+
namespace, exists := os.LookupEnv("POD_NAMESPACE")
180+
if !exists {
181+
return "", fmt.Errorf("could not find DVO pod")
182+
}
183+
184+
return namespace, nil
185+
}

0 commit comments

Comments
 (0)