Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the option to monitor argocd apps in other namespaces #320

Merged
merged 20 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions localdev/kubechecks/values.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
configMap:
create: true
env:
GRPC_ENFORCE_ALPN_ENABLED: false
KUBECHECKS_ADDITIONAL_APPS_NAMESPACES: "*"
KUBECHECKS_LOG_LEVEL: debug
KUBECHECKS_ENABLE_WEBHOOK_CONTROLLER: "false"
KUBECHECKS_ARGOCD_API_INSECURE: "true"
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 @@ -254,3 +258,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" {
MeNsaaH marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading