diff --git a/checks/run_checks.go b/checks/run_checks.go index d928db1..c02bc72 100644 --- a/checks/run_checks.go +++ b/checks/run_checks.go @@ -27,7 +27,7 @@ import ( ) // Run applies the filters and runs the resultant check list in parallel -func Run(ctx context.Context, client *kube.Client, checkFilter CheckFilter, diagnosticFilter DiagnosticFilter) (*CheckResult, error) { +func Run(ctx context.Context, client *kube.Client, checkFilter CheckFilter, diagnosticFilter DiagnosticFilter, objectFilter kube.ObjectsFilter) (*CheckResult, error) { objects, err := client.FetchObjects(ctx) if err != nil { return nil, err @@ -40,6 +40,8 @@ func Run(ctx context.Context, client *kube.Client, checkFilter CheckFilter, diag if len(all) == 0 { return nil, errors.New("No checks to run. Are you sure that you provided the right names for groups and checks?") } + objectFilter.Filter(objects) + var diagnostics []Diagnostic var mu sync.Mutex var g errgroup.Group diff --git a/checks/run_checks_test.go b/checks/run_checks_test.go index baff5a6..ce4a0e2 100644 --- a/checks/run_checks_test.go +++ b/checks/run_checks_test.go @@ -45,7 +45,7 @@ func TestRun(t *testing.T) { alwaysFailCheck, err := Get("always-fail") assert.NoError(t, err) - result, err := Run(context.Background(), client, filter, DiagnosticFilter{}) + result, err := Run(context.Background(), client, filter, DiagnosticFilter{},kube.ObjectsFilter{}) assert.NoError(t, err) assert.Len(t, result.Diagnostics, 1) assert.Equal(t, alwaysFailCheck.Name(), result.Diagnostics[0].Check) diff --git a/cmd/clusterlint/main.go b/cmd/clusterlint/main.go index f93a7d6..d85e340 100644 --- a/cmd/clusterlint/main.go +++ b/cmd/clusterlint/main.go @@ -96,6 +96,14 @@ func main() { Name: "C, ignore-checks", Usage: "run a specific check", }, + cli.StringSliceFlag{ + Name: "n, namespaces", + Usage: "run checks in specific namespace", + }, + cli.StringSliceFlag{ + Name: "N, ignore-namespaces", + Usage: "run checks not in specific namespace", + }, cli.StringFlag{ Name: "output, o", Usage: "output format [text|json]. Default: text", @@ -173,7 +181,12 @@ func runChecks(c *cli.Context) error { diagnosticFilter := checks.DiagnosticFilter{Severity: checks.Severity(c.String("level"))} - output, err := checks.Run(context.Background(), client, filter, diagnosticFilter) + objectFilter, err := kube.NewObjectsFilter(c.StringSlice("n"), c.StringSlice("N")) + if err != nil { + return err + } + + output, err := checks.Run(context.Background(), client, filter, diagnosticFilter, objectFilter) if err != nil { return err } diff --git a/kube/helper.go b/kube/helper.go new file mode 100644 index 0000000..75b73e9 --- /dev/null +++ b/kube/helper.go @@ -0,0 +1,28 @@ +/* +Copyright 2019 DigitalOcean + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import "strings" + +func contains(list []string, name string) bool { + for _, l := range list { + if strings.TrimSpace(l) == name { + return true + } + } + return false +} diff --git a/kube/objects_filter.go b/kube/objects_filter.go new file mode 100644 index 0000000..741823b --- /dev/null +++ b/kube/objects_filter.go @@ -0,0 +1,197 @@ +/* +Copyright 2019 DigitalOcean + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + // Load client-go authentication plugins + "fmt" + corev1 "k8s.io/api/core/v1" + _ "k8s.io/client-go/plugin/pkg/client/auth" +) +// ObjectsFilter stores names of namespaces that needs to be included or excluded while running checks +type ObjectsFilter struct { + IncludeNamespaces []string + ExcludeNamespaces []string +} + + +// NewObjectsFilter is a constructor to initialize an instance of ObjectsFilter +func NewObjectsFilter(includeNamespaces, excludeNamespaces []string) (ObjectsFilter, error) { + if len(includeNamespaces) > 0 && len(excludeNamespaces) > 0 { + return ObjectsFilter{}, fmt.Errorf("cannot specify both include and exclude namespace conditions") + } + return ObjectsFilter{ + IncludeNamespaces: includeNamespaces, + ExcludeNamespaces: excludeNamespaces, + }, nil +} + +// FilterChecks filters all to return set of checks based on the ObjectsFilter +func (f ObjectsFilter) Filter(objects *Objects) { + if len(f.IncludeNamespaces) > 0 { + var ps []corev1.Pod + for _, p := range objects.Pods.Items { + if contains(f.IncludeNamespaces, p.Namespace) { + ps = append(ps, p) + } + } + objects.Pods.Items = ps + + var pts []corev1.PodTemplate + for _, pt := range objects.PodTemplates.Items { + if contains(f.IncludeNamespaces, pt.Namespace) { + pts = append(pts, pt) + } + } + objects.PodTemplates.Items = pts + + var pvcs []corev1.PersistentVolumeClaim + for _, pvc := range objects.PersistentVolumeClaims.Items { + if contains(f.IncludeNamespaces, pvc.Namespace) { + pvcs = append(pvcs, pvc) + } + } + objects.PersistentVolumeClaims.Items = pvcs + + var cms []corev1.ConfigMap + for _, cm := range objects.ConfigMaps.Items { + if contains(f.IncludeNamespaces, cm.Namespace) { + cms = append(cms, cm) + } + } + objects.ConfigMaps.Items = cms + + var svcs []corev1.Service + for _, svc := range objects.Services.Items { + if contains(f.IncludeNamespaces, svc.Namespace) { + svcs = append(svcs, svc) + } + } + objects.Services.Items = svcs + + var scrts []corev1.Secret + for _, scrt := range objects.Secrets.Items { + if contains(f.IncludeNamespaces, scrt.Namespace) { + scrts = append(scrts, scrt) + } + } + objects.Secrets.Items = scrts + + var sas []corev1.ServiceAccount + for _, sa := range objects.ServiceAccounts.Items { + if contains(f.IncludeNamespaces, sa.Namespace) { + sas = append(sas, sa) + } + } + objects.ServiceAccounts.Items = sas + + var rqs []corev1.ResourceQuota + for _, rq := range objects.ResourceQuotas.Items { + if contains(f.IncludeNamespaces, rq.Namespace) { + rqs = append(rqs, rq) + } + } + objects.ResourceQuotas.Items = rqs + + var lrs []corev1.LimitRange + for _, lr := range objects.LimitRanges.Items { + if contains(f.IncludeNamespaces, lr.Namespace) { + lrs = append(lrs, lr) + } + } + objects.LimitRanges.Items = lrs + + return + } + + if len(f.ExcludeNamespaces) > 0 { + var ps []corev1.Pod + for _, p := range objects.Pods.Items { + if !contains(f.ExcludeNamespaces, p.Namespace) { + ps = append(ps, p) + } + } + objects.Pods.Items = ps + + var pts []corev1.PodTemplate + for _, pt := range objects.PodTemplates.Items { + if !contains(f.ExcludeNamespaces, pt.Namespace) { + pts = append(pts, pt) + } + } + objects.PodTemplates.Items = pts + + var pvcs []corev1.PersistentVolumeClaim + for _, pvc := range objects.PersistentVolumeClaims.Items { + if !contains(f.ExcludeNamespaces, pvc.Namespace) { + pvcs = append(pvcs, pvc) + } + } + objects.PersistentVolumeClaims.Items = pvcs + + var cms []corev1.ConfigMap + for _, cm := range objects.ConfigMaps.Items { + if !contains(f.ExcludeNamespaces, cm.Namespace) { + cms = append(cms, cm) + } + } + objects.ConfigMaps.Items = cms + + var svcs []corev1.Service + for _, svc := range objects.Services.Items { + if !contains(f.ExcludeNamespaces, svc.Namespace) { + svcs = append(svcs, svc) + } + } + objects.Services.Items = svcs + + var scrts []corev1.Secret + for _, scrt := range objects.Secrets.Items { + if !contains(f.ExcludeNamespaces, scrt.Namespace) { + scrts = append(scrts, scrt) + } + } + objects.Secrets.Items = scrts + + var sas []corev1.ServiceAccount + for _, sa := range objects.ServiceAccounts.Items { + if !contains(f.ExcludeNamespaces, sa.Namespace) { + sas = append(sas, sa) + } + } + objects.ServiceAccounts.Items = sas + + var rqs []corev1.ResourceQuota + for _, rq := range objects.ResourceQuotas.Items { + if !contains(f.ExcludeNamespaces, rq.Namespace) { + rqs = append(rqs, rq) + } + } + objects.ResourceQuotas.Items = rqs + + var lrs []corev1.LimitRange + for _, lr := range objects.LimitRanges.Items { + if !contains(f.ExcludeNamespaces, lr.Namespace) { + lrs = append(lrs, lr) + } + } + objects.LimitRanges.Items = lrs + + return + } +} + diff --git a/kube/objects_filter_test.go b/kube/objects_filter_test.go new file mode 100644 index 0000000..1197697 --- /dev/null +++ b/kube/objects_filter_test.go @@ -0,0 +1,120 @@ +/* +Copyright 2019 DigitalOcean + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "fmt" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNamespaceError(t *testing.T) { + _, err := NewObjectsFilter([]string{"kube-system"}, []string{"kube-system"}) + + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("cannot specify both include and exclude namespace conditions"), err) +} + +func TestFilter(t *testing.T) { + filter, err := NewObjectsFilter([]string{"namespace_1"},nil) + assert.NoError(t, err) + objects := namespaceObjects() + filter.Filter(objects) + assert.Equal(t, namespace1Objects(), objects) + + filter, err = NewObjectsFilter(nil,[]string{"namespace_2"}) + assert.NoError(t, err) + objects = namespaceObjects() + filter.Filter(objects) + assert.Equal(t, namespace1Objects(), objects) +} + +func objects() *Objects { + return &Objects{ + Pods: &corev1.PodList{}, + PodTemplates: &corev1.PodTemplateList{}, + PersistentVolumeClaims: &corev1.PersistentVolumeClaimList{}, + ConfigMaps: &corev1.ConfigMapList{}, + Services: &corev1.ServiceList{}, + Secrets: &corev1.SecretList{}, + ServiceAccounts: &corev1.ServiceAccountList{}, + ResourceQuotas: &corev1.ResourceQuotaList{}, + LimitRanges: &corev1.LimitRangeList{}, + } +} + +func namespaceObjects() *Objects { + objs := objects() + objs.Pods = &corev1.PodList{Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "pod_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pod_2", Namespace: "namespace_2"}}, + }} + objs.PodTemplates = &corev1.PodTemplateList{Items: []corev1.PodTemplate{ + {ObjectMeta: metav1.ObjectMeta{Name: "template_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "template_2", Namespace: "namespace_2"}}, + }} + objs.PersistentVolumeClaims = &corev1.PersistentVolumeClaimList{Items: []corev1.PersistentVolumeClaim{ + {ObjectMeta: metav1.ObjectMeta{Name: "pvc_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pvc_2", Namespace: "namespace_2"}}, + }} + objs.ConfigMaps = &corev1.ConfigMapList{Items: []corev1.ConfigMap{ + {ObjectMeta: metav1.ObjectMeta{Name: "cm_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "cm_2", Namespace: "namespace_2"}}, + }} + objs.Services = &corev1.ServiceList{Items: []corev1.Service{ + {ObjectMeta: metav1.ObjectMeta{Name: "svc_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "svc_2", Namespace: "namespace_2"}}, + }} + objs.Secrets = &corev1.SecretList{Items: []corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: "secret_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "secret_2", Namespace: "namespace_2"}}, + }} + objs.ServiceAccounts = &corev1.ServiceAccountList{Items: []corev1.ServiceAccount{ + {ObjectMeta: metav1.ObjectMeta{Name: "sa_1", Namespace: "namespace_1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "sa_2", Namespace: "namespace_2"}}, + }} + return objs +} + +func namespace1Objects() *Objects { + objs := objects() + objs.Pods = &corev1.PodList{Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "pod_1", Namespace: "namespace_1"}}, + }} + objs.PodTemplates = &corev1.PodTemplateList{Items: []corev1.PodTemplate{ + {ObjectMeta: metav1.ObjectMeta{Name: "template_1", Namespace: "namespace_1"}}, + }} + objs.PersistentVolumeClaims = &corev1.PersistentVolumeClaimList{Items: []corev1.PersistentVolumeClaim{ + {ObjectMeta: metav1.ObjectMeta{Name: "pvc_1", Namespace: "namespace_1"}}, + }} + objs.ConfigMaps = &corev1.ConfigMapList{Items: []corev1.ConfigMap{ + {ObjectMeta: metav1.ObjectMeta{Name: "cm_1", Namespace: "namespace_1"}}, + }} + objs.Services = &corev1.ServiceList{Items: []corev1.Service{ + {ObjectMeta: metav1.ObjectMeta{Name: "svc_1", Namespace: "namespace_1"}}, + }} + objs.Secrets = &corev1.SecretList{Items: []corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: "secret_1", Namespace: "namespace_1"}}, + }} + objs.ServiceAccounts = &corev1.ServiceAccountList{Items: []corev1.ServiceAccount{ + {ObjectMeta: metav1.ObjectMeta{Name: "sa_1", Namespace: "namespace_1"}}, + }} + return objs +} \ No newline at end of file