Skip to content

Commit 7720f9e

Browse files
authored
Implementation of k8s-discovery datagatherer (#165)
* Implementation of k8s-discovery datagatherer This makes use of the discovery client in client-go to get the server version information. We need this to use in upcoming packages. https://godoc.org/k8s.io/client-go/discovery Sadly, the fake discovery client doesn't seem to work in the same way as the dynamic one and I've not spent the time to write tests around it. However, it's operation is much simpler. Open to suggestions on how to test this in the same way as the dynamic client. This PR also updates all docs and renames the k8s datagatherer to k8s-dynamic in a backwards compatible manner. (Internal Issue: https://github.com/jetstack/preflight-platform/issues/350) Signed-off-by: Charlie Egan <[email protected]> * Add note about data returned for server version Signed-off-by: Charlie Egan <[email protected]>
1 parent a2e1965 commit 7720f9e

File tree

9 files changed

+171
-34
lines changed

9 files changed

+171
-34
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# k8s-discovery
2+
3+
This datagatherer uses the [DiscoveryClient](https://godoc.org/k8s.io/client-go/discovery#DiscoveryClient)
4+
to get API server version information.
5+
6+
Include the following in your agent config:
7+
8+
```
9+
data-gatherers:
10+
- kind: "k8s-discovery"
11+
name: "k8s-discovery"
12+
```
13+
14+
or specify a kubeconfig file:
15+
16+
```
17+
data-gatherers:
18+
- kind: "k8s-discovery"
19+
name: "k8s-discovery"
20+
config:
21+
kubeconfig: other_kube_config_path
22+
```

docs/datagatherers/k8s.md renamed to docs/datagatherers/k8s-dynamic.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# Kubernetes (k8s) Data Gatherer
1+
# Kubernetes Data Gatherer
22

3-
The Kubernetes data gatherer collects information about resources stored in
4-
the Kubernetes API.
3+
The Kubernetes dynamic data gatherer collects information about resources stored
4+
in the Kubernetes API.
55

66
## Data
77

@@ -28,16 +28,25 @@ below:
2828
```yaml
2929
data-gatherers:
3030
# basic usage
31-
- kind: "k8s/pods.v1"
32-
name: "pods"
31+
- kind: "k8s-dynamic"
32+
name: "k8s/pods"
33+
config:
34+
resource-type:
35+
resource: pods
36+
version: v1
3337

3438
# CRD usage
35-
- kind: "k8s/certificates.v1alpha2.cert-manager.io"
36-
name: "certificates"
39+
- kind: "k8s-dynamic"
40+
name: "k8s/certificates.v1alpha2.cert-manager.io"
41+
config:
42+
resource-type:
43+
group: cert-manager.io
44+
version: v1alpha2
45+
resource: certificates
3746

3847
# you might event want to gather resources from another cluster
39-
- kind: "k8s/pods.v1"
40-
name: "pods-cluster-2"
48+
- kind: "k8s-dynamic"
49+
name: "k8s/pods"
4150
config:
4251
kubeconfig: other_kube_config_path
4352
```

examples/cert-manager-agent.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@ endpoint:
55
host: "preflight.jetstack.io"
66
path: "/api/v1/datareadings"
77
data-gatherers:
8-
- kind: "k8s"
8+
- kind: "k8s-dynamic"
99
name: "k8s/secrets.v1"
1010
config:
1111
resource-type:
1212
version: v1
1313
resource: secrets
14-
- kind: "k8s"
14+
- kind: "k8s-dynamic"
1515
name: "k8s/certificates.v1alpha2.cert-manager.io"
1616
config:
1717
resource-type:
1818
group: cert-manager.io
1919
version: v1alpha2
2020
resource: certificates
21-
- kind: "k8s"
21+
- kind: "k8s-dynamic"
2222
name: "k8s/ingresses.v1beta1.networking.k8s.io"
2323
config:
2424
resource-type:
2525
group: networking.k8s.io
2626
version: v1beta1
2727
resource: ingresses
28-
- kind: "k8s"
28+
- kind: "k8s-dynamic"
2929
name: "k8s/certificaterequests.v1alpha2.cert-manager.io"
3030
config:
3131
resource-type:

pkg/agent/config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ func (dg *dataGatherer) UnmarshalYAML(unmarshal func(interface{}) error) error {
8787
case "aks":
8888
cfg = &aks.Config{}
8989
case "k8s":
90-
cfg = &k8s.Config{}
90+
cfg = &k8s.ConfigDynamic{}
91+
case "k8s-dynamic":
92+
cfg = &k8s.ConfigDynamic{}
93+
case "k8s-discovery":
94+
cfg = &k8s.ConfigDiscovery{}
9195
case "local":
9296
cfg = &local.Config{}
9397
// dummy dataGatherer is just used for testing

pkg/datagatherer/k8s/client.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package k8s
33

44
import (
55
"github.com/pkg/errors"
6+
"k8s.io/client-go/discovery"
67
"k8s.io/client-go/dynamic"
78
"k8s.io/client-go/rest"
89
"k8s.io/client-go/tools/clientcmd"
@@ -23,6 +24,25 @@ func NewDynamicClient(kubeconfigPath string) (dynamic.Interface, error) {
2324
return cl, nil
2425
}
2526

27+
// NewDiscoveryClient creates a new 'discovery' client using the provided
28+
// kubeconfig. If kubeconfigPath is not set/empty, it will attempt to load
29+
// configuration using the default loading rules.
30+
func NewDiscoveryClient(kubeconfigPath string) (discovery.DiscoveryClient, error) {
31+
var discoveryClient *discovery.DiscoveryClient
32+
33+
cfg, err := loadRESTConfig(kubeconfigPath)
34+
if err != nil {
35+
return *discoveryClient, errors.WithStack(err)
36+
}
37+
38+
discoveryClient, err = discovery.NewDiscoveryClientForConfig(cfg)
39+
if err != nil {
40+
return *discoveryClient, errors.WithStack(err)
41+
}
42+
43+
return *discoveryClient, nil
44+
}
45+
2646
func loadRESTConfig(path string) (*rest.Config, error) {
2747
switch path {
2848
// If the kubeconfig path is not provided, use the default loading rules

pkg/datagatherer/k8s/client_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ func TestNewDynamicClient_InferredKubeconfig(t *testing.T) {
3434
}
3535
}
3636

37+
func TestNewDiscoveryClient_ExplicitKubeconfig(t *testing.T) {
38+
kc := createValidTestConfig()
39+
path := writeConfigToFile(t, kc)
40+
_, err := NewDiscoveryClient(path)
41+
if err != nil {
42+
t.Error("failed to create client: ", err)
43+
}
44+
}
45+
46+
func TestNewDiscoveryClient_InferredKubeconfig(t *testing.T) {
47+
kc := createValidTestConfig()
48+
path := writeConfigToFile(t, kc)
49+
cleanupFn := temporarilySetEnv("KUBECONFIG", path)
50+
defer cleanupFn()
51+
_, err := NewDiscoveryClient("")
52+
if err != nil {
53+
t.Error("failed to create client: ", err)
54+
}
55+
}
56+
3757
func writeConfigToFile(t *testing.T, cfg clientcmdapi.Config) string {
3858
f, err := ioutil.TempFile("", "testcase-*")
3959
if err != nil {

pkg/datagatherer/k8s/discovery.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/jetstack/preflight/pkg/datagatherer"
8+
"k8s.io/client-go/discovery"
9+
)
10+
11+
// ConfigDiscovery contains the configuration for the k8s-discovery data-gatherer
12+
type ConfigDiscovery struct {
13+
// KubeConfigPath is the path to the kubeconfig file. If empty, will assume it runs in-cluster.
14+
KubeConfigPath string `yaml:"kubeconfig"`
15+
}
16+
17+
// UnmarshalYAML unmarshals the Config resolving GroupVersionResource.
18+
func (c *ConfigDiscovery) UnmarshalYAML(unmarshal func(interface{}) error) error {
19+
aux := struct {
20+
KubeConfigPath string `yaml:"kubeconfig"`
21+
}{}
22+
err := unmarshal(&aux)
23+
if err != nil {
24+
return err
25+
}
26+
27+
c.KubeConfigPath = aux.KubeConfigPath
28+
29+
return nil
30+
}
31+
32+
// NewDataGatherer constructs a new instance of the generic K8s data-gatherer for the provided
33+
// GroupVersionResource.
34+
func (c *ConfigDiscovery) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) {
35+
cl, err := NewDiscoveryClient(c.KubeConfigPath)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
return &DataGathererDiscovery{cl: cl}, nil
41+
}
42+
43+
// DataGathererDiscovery stores the config for a k8s-discovery datagatherer
44+
type DataGathererDiscovery struct {
45+
// The 'discovery' client used for fetching data.
46+
cl discovery.DiscoveryClient
47+
}
48+
49+
// Fetch will fetch discovery data from the apiserver, or return an error
50+
func (g *DataGathererDiscovery) Fetch() (interface{}, error) {
51+
data, err := g.cl.ServerVersion()
52+
if err != nil {
53+
return nil, fmt.Errorf("failed to get server version: %v", err)
54+
}
55+
56+
response := map[string]interface{}{
57+
// data has type Info: https://godoc.org/k8s.io/apimachinery/pkg/version#Info
58+
"server_version": data,
59+
}
60+
61+
return response, nil
62+
}

pkg/datagatherer/k8s/generic.go renamed to pkg/datagatherer/k8s/dynamic.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
"k8s.io/client-go/kubernetes/scheme"
1616
)
1717

18-
// Config contains the configuration for the data-gatherer.
19-
type Config struct {
18+
// ConfigDynamic contains the configuration for the data-gatherer.
19+
type ConfigDynamic struct {
2020
// KubeConfigPath is the path to the kubeconfig file. If empty, will assume it runs in-cluster.
2121
KubeConfigPath string `yaml:"kubeconfig"`
2222
// GroupVersionResource identifies the resource type to gather.
@@ -27,8 +27,8 @@ type Config struct {
2727
IncludeNamespaces []string `yaml:"include-namespaces"`
2828
}
2929

30-
// UnmarshalYAML unmarshals the Config resolving GroupVersionResource.
31-
func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
30+
// UnmarshalYAML unmarshals the ConfigDynamic resolving GroupVersionResource.
31+
func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(interface{}) error) error {
3232
aux := struct {
3333
KubeConfigPath string `yaml:"kubeconfig"`
3434
ResourceType struct {
@@ -53,7 +53,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
5353
}
5454

5555
// validate validates the configuration.
56-
func (c *Config) validate() error {
56+
func (c *ConfigDynamic) validate() error {
5757
var errors []string
5858
if len(c.ExcludeNamespaces) > 0 && len(c.IncludeNamespaces) > 0 {
5959
errors = append(errors, "cannot set excluded and included namespaces")
@@ -72,7 +72,7 @@ func (c *Config) validate() error {
7272

7373
// NewDataGatherer constructs a new instance of the generic K8s data-gatherer for the provided
7474
// GroupVersionResource.
75-
func (c *Config) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) {
75+
func (c *ConfigDynamic) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) {
7676
cl, err := NewDynamicClient(c.KubeConfigPath)
7777
if err != nil {
7878
return nil, err
@@ -81,26 +81,26 @@ func (c *Config) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer
8181
return c.newDataGathererWithClient(cl)
8282
}
8383

84-
func (c *Config) newDataGathererWithClient(cl dynamic.Interface) (datagatherer.DataGatherer, error) {
84+
func (c *ConfigDynamic) newDataGathererWithClient(cl dynamic.Interface) (datagatherer.DataGatherer, error) {
8585
if err := c.validate(); err != nil {
8686
return nil, err
8787
}
8888

89-
return &DataGatherer{
89+
return &DataGathererDynamic{
9090
cl: cl,
9191
groupVersionResource: c.GroupVersionResource,
9292
fieldSelector: generateFieldSelector(c.ExcludeNamespaces),
9393
namespaces: c.IncludeNamespaces,
9494
}, nil
9595
}
9696

97-
// DataGatherer is a generic gatherer for Kubernetes. It knows how to request
97+
// DataGathererDynamic is a generic gatherer for Kubernetes. It knows how to request
9898
// a list of generic resources from the Kubernetes apiserver.
9999
// It does not deserialize the objects into structured data, instead utilising
100100
// the Kubernetes `Unstructured` type for data handling.
101101
// This is to allow us to support arbitrary CRDs and resources that Preflight
102102
// does not have registered as part of its `runtime.Scheme`.
103-
type DataGatherer struct {
103+
type DataGathererDynamic struct {
104104
// The 'dynamic' client used for fetching data.
105105
cl dynamic.Interface
106106
// groupVersionResource is the name of the API group, version and resource
@@ -118,7 +118,7 @@ type DataGatherer struct {
118118

119119
// Fetch will fetch the requested data from the apiserver, or return an error
120120
// if fetching the data fails.
121-
func (g *DataGatherer) Fetch() (interface{}, error) {
121+
func (g *DataGathererDynamic) Fetch() (interface{}, error) {
122122
if g.groupVersionResource.Resource == "" {
123123
return nil, fmt.Errorf("resource type must be specified")
124124
}

pkg/datagatherer/k8s/generic_test.go renamed to pkg/datagatherer/k8s/dynamic_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func asUnstructuredList(items ...*unstructured.Unstructured) *unstructured.Unstr
6262
}
6363

6464
func TestNewDataGathererWithClient(t *testing.T) {
65-
config := Config{
65+
config := ConfigDynamic{
6666
IncludeNamespaces: []string{"a"},
6767
GroupVersionResource: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
6868
}
@@ -73,7 +73,7 @@ func TestNewDataGathererWithClient(t *testing.T) {
7373
t.Errorf("expected no error but got: %v", err)
7474
}
7575

76-
expected := &DataGatherer{
76+
expected := &DataGathererDynamic{
7777
cl: cl,
7878
groupVersionResource: config.GroupVersionResource,
7979
// it's important that the namespaces are set as the IncludeNamespaces
@@ -168,7 +168,7 @@ func TestGenericGatherer_Fetch(t *testing.T) {
168168
for name, test := range tests {
169169
t.Run(name, func(t *testing.T) {
170170
cl := fake.NewSimpleDynamicClient(emptyScheme, test.objects...)
171-
g := DataGatherer{
171+
g := DataGathererDynamic{
172172
cl: cl,
173173
groupVersionResource: test.gvr,
174174
// if empty, namespaces will default to []string{""} during
@@ -190,7 +190,7 @@ func TestGenericGatherer_Fetch(t *testing.T) {
190190
}
191191
}
192192

193-
func TestUnmarshalConfig(t *testing.T) {
193+
func TestUnmarshalGenericConfig(t *testing.T) {
194194
textCfg := `
195195
kubeconfig: "/home/someone/.kube/config"
196196
resource-type:
@@ -213,7 +213,7 @@ exclude-namespaces:
213213
"my-namespace",
214214
}
215215

216-
cfg := Config{}
216+
cfg := ConfigDynamic{}
217217
err := yaml.Unmarshal([]byte(textCfg), &cfg)
218218
if err != nil {
219219
t.Fatalf("unexpected error: %+v", err)
@@ -232,13 +232,13 @@ exclude-namespaces:
232232
}
233233
}
234234

235-
func TestConfigValidate(t *testing.T) {
235+
func TestConfigDynamicValidate(t *testing.T) {
236236
tests := []struct {
237-
Config Config
237+
Config ConfigDynamic
238238
ExpectedError string
239239
}{
240240
{
241-
Config: Config{
241+
Config: ConfigDynamic{
242242
GroupVersionResource: schema.GroupVersionResource{
243243
Group: "",
244244
Version: "",
@@ -248,7 +248,7 @@ func TestConfigValidate(t *testing.T) {
248248
ExpectedError: "invalid configuration: GroupVersionResource.Resource cannot be empty",
249249
},
250250
{
251-
Config: Config{
251+
Config: ConfigDynamic{
252252
IncludeNamespaces: []string{"a"},
253253
ExcludeNamespaces: []string{"b"},
254254
},

0 commit comments

Comments
 (0)