Skip to content

Commit

Permalink
New keel.sh/monitorContainers annotation to allow explicitly setting …
Browse files Browse the repository at this point in the history
…which containers should be monitored (#777)

* Added new keel.sh/monitorContainers annotation to allow providing an explicit regular expression that will filter which containers Keel will interact with. If left empy, it will preserve previous behaviour (all containers).
* Support for debug parameters: Context and MasterUrl
  • Loading branch information
david-garcia-garcia authored Nov 5, 2024
1 parent 0473dc6 commit 10acf52
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 592 deletions.
11 changes: 2 additions & 9 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,12 @@ envsettings.ps1
envsettings.ps1.template
helpers.ps1
LICENSE
compose.yml
compose.tests.yml
compose*
build.ps1
azure-pipelines.yml
.gitignore
.drone.yml
readme.md
serviceaccount/*
chart/*
Dockerfile
Dockerfile.debug
Dockerfile.aarch64
Dockerfile.armhf
Dockerfile.debian
Dockerfile.local
Dockerfile.tests
Dockerfile*
12 changes: 11 additions & 1 deletion cmd/keel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ const (

// kubernetes config, if empty - will default to InCluster
const (
EnvKubernetesConfig = "KUBERNETES_CONFIG"
EnvKubernetesConfig = "KUBERNETES_CONFIG"
EnvKubernetesMasterUrl = "KUBERNETES_MASTERURL"
EnvKubernetesContext = "KUBERNETES_CONTEXT"
)

// EnvDebug - set to 1 or anything else to enable debug logging
Expand Down Expand Up @@ -171,6 +173,14 @@ func main() {
k8sCfg.ConfigPath = os.Getenv(EnvKubernetesConfig)
}

if os.Getenv(EnvKubernetesMasterUrl) != "" {
k8sCfg.MasterUrl = os.Getenv(EnvKubernetesMasterUrl)
}

if os.Getenv(EnvKubernetesContext) != "" {
k8sCfg.CurrentContext = os.Getenv(EnvKubernetesContext)
}

k8sCfg.InCluster = *inCluster

implementer, err := kubernetes.NewKubernetesImplementer(k8sCfg)
Expand Down
555 changes: 0 additions & 555 deletions go.sum

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions internal/k8s/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
core_v1 "k8s.io/api/core/v1"
)

func getContainerImages(containers []core_v1.Container) []string {
func getContainerImages(containers []core_v1.Container, filter ContainerFilter) []string {
var images []string
for _, c := range containers {
images = append(images, c.Image)
if filter == nil || filter(c) {
images = append(images, c.Image)
}
}

return images
Expand Down Expand Up @@ -64,7 +66,7 @@ func updateDaemonsetSetContainer(s *apps_v1.DaemonSet, index int, image string)
func updateDaemonsetSetInitContainer(s *apps_v1.DaemonSet, index int, image string) {
s.Spec.Template.Spec.InitContainers[index].Image = image
}

// cron

func getCronJobIdentifier(s *batch_v1.CronJob) string {
Expand All @@ -78,4 +80,3 @@ func updateCronJobContainer(s *batch_v1.CronJob, index int, image string) {
func updateCronJobInitContainer(s *batch_v1.CronJob, index int, image string) {
s.Spec.JobTemplate.Spec.Template.Spec.InitContainers[index].Image = image
}

24 changes: 13 additions & 11 deletions internal/k8s/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type GenericResource struct {

type genericResource []*GenericResource

type ContainerFilter func(container core_v1.Container) bool

func (c genericResource) Len() int {
return len(c)
}
Expand Down Expand Up @@ -59,7 +61,7 @@ func NewGenericResource(obj interface{}) (*GenericResource, error) {
}

func (r *GenericResource) String() string {
return fmt.Sprintf("%s/%s/%s images: %s", r.Kind(), r.Namespace, r.Name, strings.Join(r.GetImages(), ", "))
return fmt.Sprintf("%s/%s/%s images: %s", r.Kind(), r.Namespace, r.Name, strings.Join(r.GetImages(nil), ", "))
}

// DeepCopy uses an autogenerated deepcopy functions, copying the receiver, creating a new GenericResource
Expand Down Expand Up @@ -261,31 +263,31 @@ func (r *GenericResource) GetImagePullSecrets() (secrets []string) {
}

// GetImages - returns images used by this resource
func (r *GenericResource) GetImages() (images []string) {
func (r *GenericResource) GetImages(filter ContainerFilter) (images []string) {
switch obj := r.obj.(type) {
case *apps_v1.Deployment:
return getContainerImages(obj.Spec.Template.Spec.Containers)
return getContainerImages(obj.Spec.Template.Spec.Containers, filter)
case *apps_v1.StatefulSet:
return getContainerImages(obj.Spec.Template.Spec.Containers)
return getContainerImages(obj.Spec.Template.Spec.Containers, filter)
case *apps_v1.DaemonSet:
return getContainerImages(obj.Spec.Template.Spec.Containers)
return getContainerImages(obj.Spec.Template.Spec.Containers, filter)
case *batch_v1.CronJob:
return getContainerImages(obj.Spec.JobTemplate.Spec.Template.Spec.Containers)
return getContainerImages(obj.Spec.JobTemplate.Spec.Template.Spec.Containers, filter)
}
return
}

// GetInitImages - returns init images used by this resource
func (r *GenericResource) GetInitImages() (images []string) {
func (r *GenericResource) GetInitImages(filter ContainerFilter) (images []string) {
switch obj := r.obj.(type) {
case *apps_v1.Deployment:
return getContainerImages(obj.Spec.Template.Spec.InitContainers)
return getContainerImages(obj.Spec.Template.Spec.InitContainers, filter)
case *apps_v1.StatefulSet:
return getContainerImages(obj.Spec.Template.Spec.InitContainers)
return getContainerImages(obj.Spec.Template.Spec.InitContainers, filter)
case *apps_v1.DaemonSet:
return getContainerImages(obj.Spec.Template.Spec.InitContainers)
return getContainerImages(obj.Spec.Template.Spec.InitContainers, filter)
case *batch_v1.CronJob:
return getContainerImages(obj.Spec.JobTemplate.Spec.Template.Spec.InitContainers)
return getContainerImages(obj.Spec.JobTemplate.Spec.Template.Spec.InitContainers, filter)
}
return
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/http/resources_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"github.com/keel-hq/keel/internal/k8s"
"github.com/keel-hq/keel/internal/policy"

"github.com/keel-hq/keel/provider/kubernetes"
)

type resource struct {
Expand All @@ -29,6 +31,7 @@ func (s *TriggerServer) resourcesHandler(resp http.ResponseWriter, req *http.Req
for _, v := range vals {

p := policy.GetPolicyFromLabelsOrAnnotations(v.GetLabels(), v.GetAnnotations())
filterFunc := kubernetes.GetMonitorContainersFromMeta(v.GetLabels(), v.GetAnnotations())

res = append(res, resource{
Provider: "kubernetes",
Expand All @@ -39,7 +42,7 @@ func (s *TriggerServer) resourcesHandler(resp http.ResponseWriter, req *http.Req
Policy: p.Name(),
Labels: v.GetLabels(),
Annotations: v.GetAnnotations(),
Images: v.GetImages(),
Images: v.GetImages(filterFunc),
Status: v.GetStatus(),
})
}
Expand Down
19 changes: 13 additions & 6 deletions provider/kubernetes/implementer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package kubernetes
import (
"context"
"fmt"

"github.com/keel-hq/keel/internal/k8s"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

apps_v1 "k8s.io/api/apps/v1"
batch_v1 "k8s.io/api/batch/v1"
Expand All @@ -13,10 +14,8 @@ import (
"k8s.io/client-go/kubernetes"
core_v1 "k8s.io/client-go/kubernetes/typed/core/v1"

"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

log "github.com/sirupsen/logrus"
"k8s.io/client-go/rest"
)

// Implementer - thing wrapper around currently used k8s APIs
Expand Down Expand Up @@ -44,7 +43,12 @@ type Opts struct {
// if set - kube config options will be ignored
InCluster bool
ConfigPath string
Master string
// Override the API server URL
MasterUrl string
// If multiple context in config path, the context to use
CurrentContext string
// Unused, possibly legacy
Master string
}

// NewKubernetesImplementer - create new k8s implementer
Expand All @@ -63,7 +67,10 @@ func NewKubernetesImplementer(opts *Opts) (*KubernetesImplementer, error) {
log.Info("provider.kubernetes: using in-cluster configuration")
} else if opts.ConfigPath != "" {
var err error
cfg, err = clientcmd.BuildConfigFromFlags("", opts.ConfigPath)
cfg, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: opts.ConfigPath},
&clientcmd.ConfigOverrides{CurrentContext: opts.CurrentContext, ClusterInfo: clientcmdapi.Cluster{Server: opts.MasterUrl}}).ClientConfig()

if err != nil {
log.WithFields(log.Fields{
"error": err,
Expand Down
70 changes: 65 additions & 5 deletions provider/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,52 @@ func getImagePullSecretFromMeta(labels map[string]string, annotations map[string
return ""
}

func GetMonitorContainersFromMeta(labels map[string]string, annotations map[string]string) k8s.ContainerFilter {
monitorContainersRegex := getMonitorContainersFromMeta(labels, annotations)
filterFunc := func(container v1.Container) bool {
return monitorContainersRegex.MatchString(container.Name)
}
return filterFunc
}

/**
*
*/
func getMonitorContainersFromMeta(labels map[string]string, annotations map[string]string) *regexp.Regexp {

searchKey := strings.ToLower(types.KeelMonitorContainers)

for k, v := range labels {
if strings.ToLower(k) == searchKey {
result, err := regexp.Compile(v)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"regex": v,
}).Error("provider.kubernetes: failed to parse regular expression.")
continue
}
return result
}
}

for k, v := range annotations {
if strings.ToLower(k) == searchKey {
result, err := regexp.Compile(v)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"regex": v,
}).Error("provider.kubernetes: failed to parse regular expression.")
continue
}
return result
}
}

return regexp.MustCompile(".*") // Match all to preserve previous behavior
}

func getInitContainerTrackingFromMeta(labels map[string]string, annotations map[string]string) bool {

searchKey := strings.ToLower(types.KeelInitContainerAnnotation)
Expand All @@ -169,6 +215,7 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
var trackedImages []*types.TrackedImage

for _, gr := range p.cache.Values() {

labels := gr.GetLabels()
annotations := gr.GetAnnotations()

Expand Down Expand Up @@ -205,10 +252,13 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
}
secrets = append(secrets, gr.GetImagePullSecrets()...)

images := gr.GetImages()
filterFunc := GetMonitorContainersFromMeta(annotations, labels)

images := gr.GetImages(filterFunc)
if getInitContainerTrackingFromMeta(labels, annotations) {
images = append(images, gr.GetInitImages()...)
images = append(images, gr.GetInitImages(filterFunc)...)
}

for _, img := range images {
ref, err := image.Parse(img)
if err != nil {
Expand All @@ -220,6 +270,7 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
}).Error("provider.kubernetes: failed to parse image")
continue
}

svp := make(map[string]string)

semverTag, err := semver.NewVersion(ref.Tag())
Expand Down Expand Up @@ -285,17 +336,26 @@ func (p *Provider) processEvent(event *types.Event) (updated []*k8s.GenericResou

func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*k8s.GenericResource, err error) {
for _, plan := range plans {

resource := plan.Resource

annotations := resource.GetAnnotations()
labels := resource.GetLabels()

notificationChannels := types.ParseEventNotificationChannels(annotations)
containerFilterFunction := GetMonitorContainersFromMeta(labels, annotations)
trackInitContainers := getInitContainerTrackingFromMeta(labels, annotations)

images := resource.GetImages(containerFilterFunction)
if trackInitContainers {
images = append(images, resource.GetInitImages(containerFilterFunction)...)
}

p.sender.Send(types.EventNotification{
ResourceKind: resource.Kind(),
Identifier: resource.Identifier,
Name: "preparing to update resource",
Message: fmt.Sprintf("Preparing to update %s %s/%s %s->%s (%s)", resource.Kind(), resource.Namespace, resource.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(resource.GetImages(), ", ")),
Message: fmt.Sprintf("Preparing to update %s %s/%s %s->%s (%s)", resource.Kind(), resource.Namespace, resource.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(images, ", ")),
CreatedAt: time.Now(),
Type: types.NotificationPreDeploymentUpdate,
Level: types.LevelDebug,
Expand Down Expand Up @@ -357,9 +417,9 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*k8s.Generi
var msg string
releaseNotes := types.ParseReleaseNotesURL(resource.GetAnnotations())
if releaseNotes != "" {
msg = fmt.Sprintf("Successfully updated %s %s/%s %s->%s (%s). Release notes: %s", resource.Kind(), resource.Namespace, resource.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(resource.GetImages(), ", "), releaseNotes)
msg = fmt.Sprintf("Successfully updated %s %s/%s %s->%s (%s). Release notes: %s", resource.Kind(), resource.Namespace, resource.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(images, ", "), releaseNotes)
} else {
msg = fmt.Sprintf("Successfully updated %s %s/%s %s->%s (%s)", resource.Kind(), resource.Namespace, resource.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(resource.GetImages(), ", "))
msg = fmt.Sprintf("Successfully updated %s %s/%s %s->%s (%s)", resource.Kind(), resource.Namespace, resource.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(images, ", "))
}

err = p.sender.Send(types.EventNotification{
Expand Down
9 changes: 9 additions & 0 deletions provider/kubernetes/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ func checkForUpdate(plc policy.Policy, repo *types.Repository, resource *k8s.Gen
"policy": plc.Name(),
}).Debug("provider.kubernetes.checkVersionedDeployment: keel policy found, checking resource...")
shouldUpdateDeployment = false

containerFilterFunc := GetMonitorContainersFromMeta(resource.GetAnnotations(), resource.GetLabels())

if schedule, ok := resource.GetAnnotations()[types.KeelInitContainerAnnotation]; ok && schedule == "true" {
for idx, c := range resource.InitContainers() {
if !containerFilterFunc(c) {
continue
}
containerImageRef, err := image.Parse(c.Image)
if err != nil {
log.WithFields(log.Fields{
Expand Down Expand Up @@ -90,6 +96,9 @@ func checkForUpdate(plc policy.Policy, repo *types.Repository, resource *k8s.Gen
}
}
for idx, c := range resource.Containers() {
if !containerFilterFunc(c) {
continue
}
containerImageRef, err := image.Parse(c.Image)
if err != nil {
log.WithFields(log.Fields{
Expand Down
18 changes: 18 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,24 @@ To test Keel while developing:
3. Build Keel from `cmd/keel` directory.
4. Start Keel with: `keel --no-incluster`. This will use Kubeconfig from your home.

### Debugging Keel on Windows

```powershell
# Ensure we have gcc and go
choco upgrade mingw -y
choco upgrade golang -y
# Move and build
cd cmd/keel
go build
$Env:XDG_DATA_HOME = $Env:APPDATA; # Data volume for the local database
$Env:HOME = $Env:USERPROFILE; # This is where the .kube/config will be read from
$Env:KUBERNETES_CONTEXT = "mycontext" #Use if you have more than one context in .kube/config
.\keel --no-incluster
```

### Running unit tests

Get a test parser (makes output nice):
Expand Down
Loading

0 comments on commit 10acf52

Please sign in to comment.