Skip to content

Commit dbfe5f6

Browse files
authored
Scrape only Included namespaces when set to allow more restricted RBAC (#156)
* Test empty gvr validation Signed-off-by: Charlie Egan <[email protected]> * Add IncludedNamespaces Signed-off-by: Charlie Egan <[email protected]> * Support fetching from many namespaces Signed-off-by: Charlie Egan <[email protected]> * Fetch mutiple namespaces with many requests Signed-off-by: Charlie Egan <[email protected]> * Add test for setting of namespaces from config Signed-off-by: Charlie Egan <[email protected]> * Fix typo Signed-off-by: Charlie Egan <[email protected]>
1 parent 764397f commit dbfe5f6

File tree

2 files changed

+126
-21
lines changed

2 files changed

+126
-21
lines changed

pkg/datagatherer/k8s/generic.go

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package k8s
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/jetstack/preflight/pkg/datagatherer"
89
"github.com/pkg/errors"
@@ -22,6 +23,8 @@ type Config struct {
2223
GroupVersionResource schema.GroupVersionResource
2324
// ExcludeNamespaces is a list of namespaces to exclude.
2425
ExcludeNamespaces []string `yaml:"exclude-namespaces"`
26+
// IncludeNamespaces is a list of namespaces to include.
27+
IncludeNamespaces []string `yaml:"include-namespaces"`
2528
}
2629

2730
// UnmarshalYAML unmarshals the Config resolving GroupVersionResource.
@@ -51,8 +54,17 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
5154

5255
// validate validates the configuration.
5356
func (c *Config) validate() error {
57+
var errors []string
58+
if len(c.ExcludeNamespaces) > 0 && len(c.IncludeNamespaces) > 0 {
59+
errors = append(errors, "cannot set excluded and included namespaces")
60+
}
61+
5462
if c.GroupVersionResource.Resource == "" {
55-
return fmt.Errorf("invalid configuration: GroupVersionResource.Resource cannot be empty")
63+
errors = append(errors, "invalid configuration: GroupVersionResource.Resource cannot be empty")
64+
}
65+
66+
if len(errors) > 0 {
67+
return fmt.Errorf(strings.Join(errors, ", "))
5668
}
5769

5870
return nil
@@ -61,19 +73,24 @@ func (c *Config) validate() error {
6173
// NewDataGatherer constructs a new instance of the generic K8s data-gatherer for the provided
6274
// GroupVersionResource.
6375
func (c *Config) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) {
64-
if err := c.validate(); err != nil {
76+
cl, err := NewDynamicClient(c.KubeConfigPath)
77+
if err != nil {
6578
return nil, err
6679
}
6780

68-
cl, err := NewDynamicClient(c.KubeConfigPath)
69-
if err != nil {
81+
return c.newDataGathererWithClient(cl)
82+
}
83+
84+
func (c *Config) newDataGathererWithClient(cl dynamic.Interface) (datagatherer.DataGatherer, error) {
85+
if err := c.validate(); err != nil {
7086
return nil, err
7187
}
7288

7389
return &DataGatherer{
7490
cl: cl,
7591
groupVersionResource: c.GroupVersionResource,
7692
fieldSelector: generateFieldSelector(c.ExcludeNamespaces),
93+
namespaces: c.IncludeNamespaces,
7794
}, nil
7895
}
7996

@@ -92,7 +109,7 @@ type DataGatherer struct {
92109
// namespace, if specified, limits the namespace of the resources returned.
93110
// This field *must* be omitted when the groupVersionResource refers to a
94111
// non-namespaced resource.
95-
namespace string
112+
namespaces []string
96113
// fieldSelector is a field selector string used to filter resources
97114
// returned by the Kubernetes API.
98115
// https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
@@ -105,19 +122,34 @@ func (g *DataGatherer) Fetch() (interface{}, error) {
105122
if g.groupVersionResource.Resource == "" {
106123
return nil, fmt.Errorf("resource type must be specified")
107124
}
108-
resourceInterface := namespaceResourceInterface(g.cl.Resource(g.groupVersionResource), g.namespace)
109-
list, err := resourceInterface.List(metav1.ListOptions{
110-
FieldSelector: g.fieldSelector,
111-
})
112-
if err != nil {
113-
return nil, errors.WithStack(err)
125+
126+
var list unstructured.UnstructuredList
127+
128+
fetchNamespaces := g.namespaces
129+
if len(fetchNamespaces) == 0 {
130+
// then they must have been looking for all namespaces
131+
fetchNamespaces = []string{""}
114132
}
133+
134+
for _, namespace := range fetchNamespaces {
135+
resourceInterface := namespaceResourceInterface(g.cl.Resource(g.groupVersionResource), namespace)
136+
namespaceList, err := resourceInterface.List(metav1.ListOptions{
137+
FieldSelector: g.fieldSelector,
138+
})
139+
if err != nil {
140+
return nil, errors.WithStack(err)
141+
}
142+
list.Object = namespaceList.Object
143+
list.Items = append(list.Items, namespaceList.Items...)
144+
}
145+
115146
// Redact Secret data
116-
err = redactList(list)
147+
err := redactList(&list)
117148
if err != nil {
118149
return nil, errors.WithStack(err)
119150
}
120-
return list, nil
151+
152+
return &list, nil
121153
}
122154

123155
func redactList(list *unstructured.UnstructuredList) error {

pkg/datagatherer/k8s/generic_test.go

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package k8s
33
import (
44
"encoding/json"
55
"reflect"
6+
"strings"
67
"testing"
78

89
"gopkg.in/yaml.v2"
@@ -60,14 +61,39 @@ func asUnstructuredList(items ...*unstructured.Unstructured) *unstructured.Unstr
6061
}
6162
}
6263

64+
func TestNewDataGathererWithClient(t *testing.T) {
65+
config := Config{
66+
IncludeNamespaces: []string{"a"},
67+
GroupVersionResource: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
68+
}
69+
cl := fake.NewSimpleDynamicClient(runtime.NewScheme())
70+
dg, err := config.newDataGathererWithClient(cl)
71+
72+
if err != nil {
73+
t.Errorf("expected no error but got: %v", err)
74+
}
75+
76+
expected := &DataGatherer{
77+
cl: cl,
78+
groupVersionResource: config.GroupVersionResource,
79+
// it's important that the namespaces are set as the IncludeNamespaces
80+
// during initialization
81+
namespaces: config.IncludeNamespaces,
82+
}
83+
84+
if !reflect.DeepEqual(dg, expected) {
85+
t.Errorf("unexpected difference: %v", diff.ObjectDiff(dg, expected))
86+
}
87+
}
88+
6389
func TestGenericGatherer_Fetch(t *testing.T) {
6490
emptyScheme := runtime.NewScheme()
6591
tests := map[string]struct {
66-
gvr schema.GroupVersionResource
67-
namespace string
68-
objects []runtime.Object
69-
expected interface{}
70-
err bool
92+
gvr schema.GroupVersionResource
93+
namespaces []string
94+
objects []runtime.Object
95+
expected interface{}
96+
err bool
7197
}{
7298
"an error should be returned if 'resource' is missing": {
7399
err: true,
@@ -85,8 +111,8 @@ func TestGenericGatherer_Fetch(t *testing.T) {
85111
),
86112
},
87113
"only Foos in the specified namespace should be returned": {
88-
gvr: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
89-
namespace: "testns",
114+
gvr: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
115+
namespaces: []string{"testns"},
90116
objects: []runtime.Object{
91117
getObject("foobar/v1", "Foo", "testfoo", "testns"),
92118
getObject("foobar/v1", "Foo", "testfoo", "nottestns"),
@@ -121,6 +147,19 @@ func TestGenericGatherer_Fetch(t *testing.T) {
121147
getSecret("anothertestsecret", "differentns", map[string]interface{}{}, false),
122148
),
123149
},
150+
"Foos in different namespaces should be returned if they are in the namespace list for the gatherer": {
151+
gvr: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
152+
namespaces: []string{"testns", "testns2"},
153+
objects: []runtime.Object{
154+
getObject("foobar/v1", "Foo", "testfoo", "testns"),
155+
getObject("foobar/v1", "Foo", "testfoo2", "testns2"),
156+
getObject("foobar/v1", "Foo", "testfoo3", "nottestns"),
157+
},
158+
expected: asUnstructuredList(
159+
getObject("foobar/v1", "Foo", "testfoo", "testns"),
160+
getObject("foobar/v1", "Foo", "testfoo2", "testns2"),
161+
),
162+
},
124163
// Note that we can't test use of fieldSelector to exclude namespaces
125164
// here as the as the fake client does not implement it.
126165
// See go/pkg/mod/k8s.io/[email protected]/dynamic/fake/simple.go:291
@@ -132,7 +171,9 @@ func TestGenericGatherer_Fetch(t *testing.T) {
132171
g := DataGatherer{
133172
cl: cl,
134173
groupVersionResource: test.gvr,
135-
namespace: test.namespace,
174+
// if empty, namespaces will default to []string{""} during
175+
// fetch to get all ns
176+
namespaces: test.namespaces,
136177
}
137178

138179
res, err := g.Fetch()
@@ -191,6 +232,38 @@ exclude-namespaces:
191232
}
192233
}
193234

235+
func TestConfigValidate(t *testing.T) {
236+
tests := []struct {
237+
Config Config
238+
ExpectedError string
239+
}{
240+
{
241+
Config: Config{
242+
GroupVersionResource: schema.GroupVersionResource{
243+
Group: "",
244+
Version: "",
245+
Resource: "",
246+
},
247+
},
248+
ExpectedError: "invalid configuration: GroupVersionResource.Resource cannot be empty",
249+
},
250+
{
251+
Config: Config{
252+
IncludeNamespaces: []string{"a"},
253+
ExcludeNamespaces: []string{"b"},
254+
},
255+
ExpectedError: "cannot set excluded and included namespaces",
256+
},
257+
}
258+
259+
for _, test := range tests {
260+
err := test.Config.validate()
261+
if !strings.Contains(err.Error(), test.ExpectedError) {
262+
t.Errorf("expected %s, got %s", test.ExpectedError, err.Error())
263+
}
264+
}
265+
}
266+
194267
func TestGenerateFieldSelector(t *testing.T) {
195268
tests := []struct {
196269
ExcludeNamespaces []string

0 commit comments

Comments
 (0)