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  }