Skip to content

Commit

Permalink
Allow the option to monitor argocd apps in other namespaces (#320)
Browse files Browse the repository at this point in the history
Signed-off-by: Abhi Kapoor <[email protected]>
  • Loading branch information
abhi-kapoor authored Dec 20, 2024
1 parent 26d99ad commit a2a67b8
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 22 deletions.
2 changes: 1 addition & 1 deletion charts/kubechecks/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: kubechecks
description: A Helm chart for kubechecks
version: 0.5.2
version: 0.5.3
type: application
maintainers:
- name: zapier
1 change: 1 addition & 0 deletions charts/kubechecks/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ commonLabels: {}
configMap:
create: false
env: {}
# KUBECHECKS_ADDITIONAL_APPS_NAMESPACES: "*"
# KUBECHECKS_ARGOCD_API_INSECURE: "false"
# KUBECHECKS_ARGOCD_API_PATH_PREFIX: /
# KUBECHECKS_ARGOCD_API_NAMESPACE: argocd
Expand Down
4 changes: 2 additions & 2 deletions cmd/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ var ControllerCmd = &cobra.Command{

// watch app modifications, if necessary
if cfg.MonitorAllApplications {
appWatcher, err := app_watcher.NewApplicationWatcher(ctr)
appWatcher, err := app_watcher.NewApplicationWatcher(ctr, ctx)
if err != nil {
log.Fatal().Err(err).Msg("failed to create watch applications")
}
go appWatcher.Run(ctx, 1)

appSetWatcher, err := app_watcher.NewApplicationSetWatcher(ctr)
appSetWatcher, err := app_watcher.NewApplicationSetWatcher(ctr, ctx)
if err != nil {
log.Fatal().Err(err).Msg("failed to create watch application sets")
}
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func init() {
stringFlag(flags, "replan-comment-msg", "comment message which re-triggers kubechecks on PR.",
newStringOpts().
withDefault("kubechecks again"))
stringSliceFlag(flags, "additional-apps-namespaces", "Additional namespaces other than the ArgoCDNamespace to monitor for applications.")

panicIfError(viper.BindPFlags(flags))
setupLogOutput()
Expand Down
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The full list of supported environment variables is described below:

|Env Var|Description|Default Value|
|-----------|-------------|------|
|`KUBECHECKS_ADDITIONAL_APPS_NAMESPACES`|Additional namespaces other than the ArgoCDNamespace to monitor for applications.|`[]`|
|`KUBECHECKS_ARGOCD_API_INSECURE`|Enable to use insecure connections over TLS to the ArgoCD API server.|`false`|
|`KUBECHECKS_ARGOCD_API_NAMESPACE`|ArgoCD namespace where the application watcher will read Custom Resource Definitions (CRD) for Application and ApplicationSet resources.|`argocd`|
|`KUBECHECKS_ARGOCD_API_PLAINTEXT`|Enable to use plaintext connections without TLS.|`false`|
Expand Down
55 changes: 46 additions & 9 deletions pkg/app_watcher/app_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ import (

appv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
informers "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/glob"
"github.com/rs/zerolog/log"
"github.com/zapier/kubechecks/pkg/appdir"
"github.com/zapier/kubechecks/pkg/container"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"

"github.com/zapier/kubechecks/pkg/appdir"
"github.com/zapier/kubechecks/pkg/config"
"github.com/zapier/kubechecks/pkg/container"
)

// ApplicationWatcher is the controller that watches ArgoCD Application resources via the Kubernetes API
Expand All @@ -34,7 +37,7 @@ type ApplicationWatcher struct {
// - kubeCfg is the Kubernetes configuration.
// - vcsToArgoMap is the mapping between VCS and Argo applications.
// - cfg is the server configuration.
func NewApplicationWatcher(ctr container.Container) (*ApplicationWatcher, error) {
func NewApplicationWatcher(ctr container.Container, ctx context.Context) (*ApplicationWatcher, error) {
if ctr.KubeClientSet == nil {
return nil, fmt.Errorf("kubeCfg cannot be nil")
}
Expand All @@ -43,7 +46,7 @@ func NewApplicationWatcher(ctr container.Container) (*ApplicationWatcher, error)
vcsToArgoMap: ctr.VcsToArgoMap,
}

appInformer, appLister := ctrl.newApplicationInformerAndLister(time.Second*30, ctr.Config)
appInformer, appLister := ctrl.newApplicationInformerAndLister(time.Second*30, ctr.Config, ctx)

ctrl.appInformer = appInformer
ctrl.appLister = appLister
Expand Down Expand Up @@ -116,6 +119,11 @@ func (ctrl *ApplicationWatcher) onApplicationDeleted(obj interface{}) {
ctrl.vcsToArgoMap.DeleteApp(app)
}

// isAppNamespaceAllowed is used by both the ApplicationWatcher and the ApplicationSetWatcher
func isAppNamespaceAllowed(meta *metav1.ObjectMeta, cfg config.ServerConfig) bool {
return meta.Namespace == cfg.ArgoCDNamespace || glob.MatchStringInList(cfg.AdditionalAppsNamespaces, meta.Namespace, glob.REGEXP)
}

/*
newApplicationInformerAndLister, is part of the ApplicationWatcher struct. It sets up a Kubernetes SharedIndexInformer
and a Lister for Argo CD Applications.
Expand All @@ -127,12 +135,41 @@ that need to observe the object.
newApplicationInformerAndLister use the data from the informer's cache to provide a read-optimized view of the cache which reduces
the load on the API Server and hides some complexity.
*/
func (ctrl *ApplicationWatcher) newApplicationInformerAndLister(refreshTimeout time.Duration, cfg config.ServerConfig) (cache.SharedIndexInformer, applisters.ApplicationLister) {
log.Debug().Msgf("Creating Application informer with namespace: %s", cfg.ArgoCDNamespace)
informer := informers.NewApplicationInformer(ctrl.applicationClientset, cfg.ArgoCDNamespace, refreshTimeout,
func (ctrl *ApplicationWatcher) newApplicationInformerAndLister(refreshTimeout time.Duration, cfg config.ServerConfig, ctx context.Context) (cache.SharedIndexInformer, applisters.ApplicationLister) {

watchNamespace := cfg.ArgoCDNamespace
// If we have at least one additional namespace configured, we need to
// watch on them all.
if len(cfg.AdditionalAppsNamespaces) > 0 {
watchNamespace = ""
}

informer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (apiruntime.Object, error) {
// We are only interested in apps that exist in namespaces the
// user wants to be enabled.
appList, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(watchNamespace).List(ctx, options)
if err != nil {
return nil, err
}
newItems := []appv1alpha1.Application{}
for _, app := range appList.Items {
if isAppNamespaceAllowed(&app.ObjectMeta, cfg) {
newItems = append(newItems, app)
}
}
appList.Items = newItems
return appList, nil
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return ctrl.applicationClientset.ArgoprojV1alpha1().Applications(watchNamespace).Watch(ctx, options)
},
},
&appv1alpha1.Application{},
refreshTimeout,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)

lister := applisters.NewApplicationLister(informer.GetIndexer())
if _, err := informer.AddEventHandler(
cache.ResourceEventHandlerFuncs{
Expand Down
52 changes: 51 additions & 1 deletion pkg/app_watcher/app_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import (
)

func initTestObjects(t *testing.T) *ApplicationWatcher {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cfg, err := config.New()
cfg.AdditionalAppsNamespaces = []string{"*"}
// Handle the error appropriately, e.g., log it or fail the test
require.NoError(t, err, "failed to create config")

Expand All @@ -40,7 +44,7 @@ func initTestObjects(t *testing.T) *ApplicationWatcher {
vcsToArgoMap: appdir.NewVcsToArgoMap("vcs-username"),
}

appInformer, appLister := ctrl.newApplicationInformerAndLister(time.Second*1, cfg)
appInformer, appLister := ctrl.newApplicationInformerAndLister(time.Second*1, cfg, ctx)
ctrl.appInformer = appInformer
ctrl.appLister = appLister

Expand Down Expand Up @@ -255,3 +259,49 @@ func TestCanProcessApp(t *testing.T) {
})
}
}

func TestIsAppNamespaceAllowed(t *testing.T) {
tests := map[string]struct {
expected bool
cfg config.ServerConfig
meta *metav1.ObjectMeta
}{
"All namespaces for application are allowed": {
expected: true,
cfg: config.ServerConfig{AdditionalAppsNamespaces: []string{"*"}},
meta: &metav1.ObjectMeta{Name: "test-app-1", Namespace: "default-ns"},
},
"Specific namespaces for application are allowed": {
expected: false,
cfg: config.ServerConfig{AdditionalAppsNamespaces: []string{"default", "kube-system"}},
meta: &metav1.ObjectMeta{Name: "test-app-1", Namespace: "default-ns"},
},
"Wildcard namespace for application is allowed": {
expected: true,
cfg: config.ServerConfig{AdditionalAppsNamespaces: []string{"default-*"}},
meta: &metav1.ObjectMeta{Name: "test-app-1", Namespace: "default-ns"},
},
"Invalid characters in namespace for application are not allowed": {
expected: false,
cfg: config.ServerConfig{AdditionalAppsNamespaces: []string{"<default-*", "kube-system"}},
meta: &metav1.ObjectMeta{Name: "test-app-1", Namespace: "default-ns"},
},
"Specific namespace for application set is allowed": {
expected: true,
cfg: config.ServerConfig{AdditionalAppsNamespaces: []string{"<default-*>", "kube-system"}},
meta: &metav1.ObjectMeta{Name: "test-appset-1", Namespace: "kube-system"},
},
"Regex in namespace for application set is allowed": {
expected: true,
cfg: config.ServerConfig{AdditionalAppsNamespaces: []string{"/^((?!kube-system).)*$/"}},
meta: &metav1.ObjectMeta{Name: "test-appset-1", Namespace: "kube-namespace"},
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
actual := isAppNamespaceAllowed(test.meta, test.cfg)
assert.Equal(t, test.expected, actual)
})
}
}
44 changes: 37 additions & 7 deletions pkg/app_watcher/appset_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (

appv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
informers "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
"github.com/rs/zerolog/log"
"github.com/zapier/kubechecks/pkg/appdir"
"github.com/zapier/kubechecks/pkg/config"
"github.com/zapier/kubechecks/pkg/container"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
)

Expand All @@ -28,7 +30,7 @@ type ApplicationSetWatcher struct {
}

// NewApplicationSetWatcher creates new instance of ApplicationWatcher.
func NewApplicationSetWatcher(ctr container.Container) (*ApplicationSetWatcher, error) {
func NewApplicationSetWatcher(ctr container.Container, ctx context.Context) (*ApplicationSetWatcher, error) {
if ctr.KubeClientSet == nil {
return nil, fmt.Errorf("kubeCfg cannot be nil")
}
Expand All @@ -37,7 +39,7 @@ func NewApplicationSetWatcher(ctr container.Container) (*ApplicationSetWatcher,
vcsToArgoMap: ctr.VcsToArgoMap,
}

appInformer, appLister := ctrl.newApplicationSetInformerAndLister(time.Second*30, ctr.Config)
appInformer, appLister := ctrl.newApplicationSetInformerAndLister(time.Second*30, ctr.Config, ctx)

ctrl.appInformer = appInformer
ctrl.appLister = appLister
Expand All @@ -61,12 +63,40 @@ func (ctrl *ApplicationSetWatcher) Run(ctx context.Context) {
<-ctx.Done()
}

func (ctrl *ApplicationSetWatcher) newApplicationSetInformerAndLister(refreshTimeout time.Duration, cfg config.ServerConfig) (cache.SharedIndexInformer, applisters.ApplicationSetLister) {
log.Debug().Msgf("Creating ApplicationSet informer with namespace: %s", cfg.ArgoCDNamespace)
informer := informers.NewApplicationSetInformer(ctrl.applicationClientset, cfg.ArgoCDNamespace, refreshTimeout,
func (ctrl *ApplicationSetWatcher) newApplicationSetInformerAndLister(refreshTimeout time.Duration, cfg config.ServerConfig, ctx context.Context) (cache.SharedIndexInformer, applisters.ApplicationSetLister) {
watchNamespace := cfg.ArgoCDNamespace
// If we have at least one additional namespace configured, we need to
// watch on them all.
if len(cfg.AdditionalAppsNamespaces) > 0 {
watchNamespace = ""
}

informer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (apiruntime.Object, error) {
// We are only interested in apps that exist in namespaces the
// user wants to be enabled.
appList, err := ctrl.applicationClientset.ArgoprojV1alpha1().ApplicationSets(watchNamespace).List(ctx, options)
if err != nil {
return nil, err
}
newItems := []appv1alpha1.ApplicationSet{}
for _, appSet := range appList.Items {
if isAppNamespaceAllowed(&appSet.ObjectMeta, cfg) {
newItems = append(newItems, appSet)
}
}
appList.Items = newItems
return appList, nil
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return ctrl.applicationClientset.ArgoprojV1alpha1().ApplicationSets(watchNamespace).Watch(ctx, options)
},
},
&appv1alpha1.ApplicationSet{},
refreshTimeout,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)

AppSetLister := applisters.NewApplicationSetLister(informer.GetIndexer())
if _, err := informer.AddEventHandler(
cache.ResourceEventHandlerFuncs{
Expand Down
7 changes: 5 additions & 2 deletions pkg/app_watcher/appset_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import (
)

func initTestObjectsForAppSets(t *testing.T) *ApplicationSetWatcher {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cfg, err := config.New()
cfg.AdditionalAppsNamespaces = []string{"*"}
// Handle the error appropriately, e.g., log it or fail the test
require.NoError(t, err, "failed to create config")

Expand Down Expand Up @@ -53,10 +57,9 @@ func initTestObjectsForAppSets(t *testing.T) *ApplicationSetWatcher {
vcsToArgoMap: appdir.NewVcsToArgoMap("vcs-username"),
}

appInformer, appLister := ctrl.newApplicationSetInformerAndLister(time.Second*1, cfg)
appInformer, appLister := ctrl.newApplicationSetInformerAndLister(time.Second*1, cfg, ctx)
ctrl.appInformer = appInformer
ctrl.appLister = appLister

return ctrl
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"reflect"
"strings"
"time"

"github.com/mitchellh/mapstructure"
Expand Down Expand Up @@ -70,6 +71,7 @@ type ServerConfig struct {
WorstPreupgradeState pkg.CommitState `mapstructure:"worst-preupgrade-state"`

// misc
AdditionalAppsNamespaces []string `mapstructure:"additional-apps-namespaces"`
FallbackK8sVersion string `mapstructure:"fallback-k8s-version"`
LabelFilter string `mapstructure:"label-filter"`
LogLevel zerolog.Level `mapstructure:"log-level"`
Expand Down Expand Up @@ -107,6 +109,12 @@ func NewWithViper(v *viper.Viper) (ServerConfig, error) {
return time.ParseDuration(input)
}

if in.String() == "string" && out.String() == "[]string" {
input := value.(string)
ns := strings.Split(input, ",")
return ns, nil
}

return value, nil
}
}); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestNew(t *testing.T) {
v.Set("argocd-api-plaintext", "true")
v.Set("worst-conftest-state", "warning")
v.Set("repo-refresh-interval", "10m")
v.Set("additional-apps-namespaces", "default,kube-system")

cfg, err := NewWithViper(v)
require.NoError(t, err)
Expand All @@ -27,4 +28,5 @@ func TestNew(t *testing.T) {
assert.Equal(t, true, cfg.ArgoCDPlainText)
assert.Equal(t, pkg.StateWarning, cfg.WorstConfTestState, "worst states can be overridden")
assert.Equal(t, time.Minute*10, cfg.RepoRefreshInterval)
assert.Equal(t, []string{"default", "kube-system"}, cfg.AdditionalAppsNamespaces)
}

0 comments on commit a2a67b8

Please sign in to comment.