github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/controller/state/kubelinter/controller.go (about) 1 package kubelinter 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "reflect" 8 "strings" 9 "time" 10 11 castaipb "github.com/castai/kvisor/api/v1/runtime" 12 "github.com/castai/kvisor/cmd/controller/kube" 13 "github.com/castai/kvisor/pkg/logging" 14 "google.golang.org/grpc" 15 "google.golang.org/grpc/encoding/gzip" 16 appsv1 "k8s.io/api/apps/v1" 17 batchv1 "k8s.io/api/batch/v1" 18 networkingv1 "k8s.io/api/networking/v1" 19 rbacv1 "k8s.io/api/rbac/v1" 20 21 "github.com/samber/lo" 22 "golang.stackrox.io/kube-linter/pkg/lintcontext" 23 corev1 "k8s.io/api/core/v1" 24 ) 25 26 type castaiClient interface { 27 KubeLinterReportIngest(ctx context.Context, in *castaipb.KubeLinterReport, opts ...grpc.CallOption) (*castaipb.KubeLinterReportIngestResponse, error) 28 } 29 30 type Config struct { 31 Enabled bool 32 ScanInterval time.Duration `validate:"required"` 33 InitDelay time.Duration 34 } 35 36 func NewController(log *logging.Logger, cfg Config, linter *Linter, castaiClient castaiClient) *Controller { 37 return &Controller{ 38 log: log.WithField("component", "kubelinter"), 39 cfg: cfg, 40 linter: linter, 41 client: castaiClient, 42 delta: newDeltaState(), 43 } 44 } 45 46 type Controller struct { 47 log *logging.Logger 48 cfg Config 49 linter *Linter 50 client castaiClient 51 delta *deltaState 52 } 53 54 func (c *Controller) RequiredTypes() []reflect.Type { 55 return []reflect.Type{ 56 reflect.TypeOf(&corev1.Pod{}), 57 reflect.TypeOf(&corev1.Namespace{}), 58 reflect.TypeOf(&corev1.Service{}), 59 reflect.TypeOf(&appsv1.Deployment{}), 60 reflect.TypeOf(&appsv1.DaemonSet{}), 61 reflect.TypeOf(&appsv1.StatefulSet{}), 62 reflect.TypeOf(&rbacv1.ClusterRoleBinding{}), 63 reflect.TypeOf(&rbacv1.RoleBinding{}), 64 reflect.TypeOf(&rbacv1.ClusterRole{}), 65 reflect.TypeOf(&rbacv1.Role{}), 66 reflect.TypeOf(&networkingv1.NetworkPolicy{}), 67 reflect.TypeOf(&networkingv1.Ingress{}), 68 reflect.TypeOf(&batchv1.Job{}), 69 reflect.TypeOf(&batchv1.CronJob{}), 70 } 71 } 72 73 func (c *Controller) Run(ctx context.Context) error { 74 c.log.Info("running") 75 defer c.log.Infof("stopping") 76 77 select { 78 case <-ctx.Done(): 79 return ctx.Err() 80 case <-time.After(c.cfg.InitDelay): 81 } 82 83 for { 84 select { 85 case <-ctx.Done(): 86 return nil 87 case <-time.After(c.cfg.ScanInterval): 88 objects := c.delta.flush() 89 if len(objects) > 0 { 90 if err := c.lintObjects(ctx, objects); err != nil && !errors.Is(err, context.Canceled) { 91 c.log.Error(err.Error()) 92 93 // put unprocessed objects back to delta queue 94 c.delta.insert(objects...) 95 } 96 } 97 } 98 } 99 } 100 101 func (c *Controller) OnAdd(obj kube.Object) { 102 c.modifyDelta(kube.EventAdd, obj) 103 } 104 105 func (c *Controller) OnUpdate(obj kube.Object) { 106 c.modifyDelta(kube.EventUpdate, obj) 107 } 108 109 func (c *Controller) OnDelete(obj kube.Object) { 110 c.modifyDelta(kube.EventDelete, obj) 111 } 112 113 func (c *Controller) modifyDelta(event kube.EventType, o kube.Object) { 114 switch o := o.(type) { 115 case *corev1.Pod: 116 // Do not process not static pods. 117 if !isStandalonePod(o) { 118 return 119 } 120 case *batchv1.Job: 121 // Skip jobs which belongs to cronjobs etc. 122 if !isStandaloneJob(o) { 123 return 124 } 125 } 126 127 switch event { 128 case kube.EventAdd: 129 c.delta.upsert(o) 130 case kube.EventUpdate: 131 c.delta.upsert(o) 132 case kube.EventDelete: 133 c.delta.delete(o) 134 } 135 } 136 137 func (c *Controller) lintObjects(ctx context.Context, objects []kube.Object) (rerr error) { 138 checks, err := c.linter.Run(lo.Map(objects, func(o kube.Object, i int) lintcontext.Object { 139 return lintcontext.Object{K8sObject: o} 140 })) 141 if err != nil { 142 return fmt.Errorf("kubelinter failed: %w", err) 143 } 144 145 ctx, cancel := context.WithTimeout(ctx, time.Second*5) 146 defer cancel() 147 148 pbReport := &castaipb.KubeLinterReport{ 149 Checks: lo.Map(checks, func(item LinterCheck, index int) *castaipb.KubeLinterCheck { 150 var passed, failed uint64 151 if item.Passed != nil { 152 passed = uint64(*item.Passed) 153 } 154 if item.Failed != nil { 155 failed = uint64(*item.Failed) 156 } 157 return &castaipb.KubeLinterCheck{ 158 ResourceUid: item.ResourceID, 159 Passed: passed, 160 Failed: failed, 161 } 162 }), 163 } 164 165 if _, err := c.client.KubeLinterReportIngest(ctx, pbReport, grpc.UseCompressor(gzip.Name)); err != nil { 166 return fmt.Errorf("can not send kubelinter checks: %w", err) 167 } 168 169 c.log.Infof("kubelinter finished, checks: %d", len(checks)) 170 return nil 171 } 172 173 func isStandalonePod(pod *corev1.Pod) bool { 174 if pod.Spec.NodeName == "" { 175 return false 176 } 177 178 // Pod created without parent. 179 if len(pod.OwnerReferences) == 0 { 180 return true 181 } 182 183 // Static pod. 184 return strings.HasSuffix(pod.ObjectMeta.Name, pod.Spec.NodeName) 185 } 186 187 func isStandaloneJob(job *batchv1.Job) bool { 188 return len(job.OwnerReferences) == 0 189 }