Skip to content

Commit 069beb4

Browse files
committed
Abstract client rest.Config behind an interface
Too bad discovery.NewDiscoveryClientForConfigOrDie() and dynamic.NewDynamicClientPool() expects a concrete *rest.Config struct rather than a kubernetes.Interface (clientset). We can still wrap this rest.Config behind an interface. That's a first step before we can, progressively, get rid of Config (and run.go), which would make pkg/ content more like reusable packages rather than a frameworkish bunch of files.
1 parent f7b5700 commit 069beb4

File tree

7 files changed

+120
-40
lines changed

7 files changed

+120
-40
lines changed

cmd/execute.go

+13-6
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,37 @@ import (
88
"github.com/spf13/viper"
99

1010
"github.com/bpineau/katafygio/config"
11+
"github.com/bpineau/katafygio/pkg/client"
1112
"github.com/bpineau/katafygio/pkg/log"
1213
"github.com/bpineau/katafygio/pkg/run"
1314
)
1415

1516
const appName = "katafygio"
1617

1718
var (
19+
restcfg client.Interface
20+
1821
// RootCmd is our main entry point, launching pkg/run.Run()
1922
RootCmd = &cobra.Command{
2023
Use: appName,
2124
Short: "Backup Kubernetes cluster as yaml files",
2225
Long: "Backup Kubernetes cluster as yaml files in a git repository.\n" +
2326
"--exclude-kind (-x) and --exclude-object (-y) may be specified several times.",
2427

25-
RunE: func(cmd *cobra.Command, args []string) error {
28+
RunE: func(cmd *cobra.Command, args []string) (err error) {
2629
resync := time.Duration(viper.GetInt("resync-interval")) * time.Second
2730
logger := log.New(viper.GetString("log.level"),
2831
viper.GetString("log.server"),
2932
viper.GetString("log.output"))
3033

34+
if restcfg == nil {
35+
restcfg, err = client.New(viper.GetString("api-server"),
36+
viper.GetString("kube-config"))
37+
if err != nil {
38+
return fmt.Errorf("failed to create a client: %v", err)
39+
}
40+
}
41+
3142
conf := &config.KfConfig{
3243
DryRun: viper.GetBool("dry-run"),
3344
DumpMode: viper.GetBool("dump-only"),
@@ -38,14 +49,10 @@ var (
3849
ExcludeKind: viper.GetStringSlice("exclude-kind"),
3950
ExcludeObject: viper.GetStringSlice("exclude-object"),
4051
HealthPort: viper.GetInt("healthcheck-port"),
52+
Client: restcfg,
4153
ResyncIntv: resync,
4254
}
4355

44-
err := conf.Init(viper.GetString("api-server"), viper.GetString("kube-config"))
45-
if err != nil {
46-
return fmt.Errorf("Failed to initialize the configuration: %v", err)
47-
}
48-
4956
run.Run(conf) // <- this is where things happen
5057
return nil
5158
},

cmd/execute_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"k8s.io/client-go/rest"
8+
)
9+
10+
type mockClient struct{}
11+
12+
func (m *mockClient) GetRestConfig() *rest.Config {
13+
return &rest.Config{}
14+
}
15+
16+
func TestRootCmd(t *testing.T) {
17+
restcfg = new(mockClient)
18+
RootCmd.SetOutput(new(bytes.Buffer))
19+
RootCmd.SetArgs([]string{
20+
"--config",
21+
"/dev/null",
22+
"--kube-config",
23+
"/dev/null",
24+
"--dry-run",
25+
"--dump-only",
26+
"--api-server",
27+
"http://192.0.2.1", // RFC 5737 reserved/unroutable
28+
"--log-level",
29+
"warning",
30+
"--log-output",
31+
"test",
32+
"--healthcheck-port",
33+
"0",
34+
"--filter",
35+
"foo=bar,spam=egg",
36+
"--resync-interval",
37+
"1",
38+
})
39+
40+
if err := Execute(); err != nil {
41+
t.Errorf("version subcommand shouldn't fail: %+v", err)
42+
}
43+
}
44+
45+
func TestVersionCmd(t *testing.T) {
46+
RootCmd.SetOutput(new(bytes.Buffer))
47+
RootCmd.SetArgs([]string{"version"})
48+
if err := RootCmd.Execute(); err != nil {
49+
t.Errorf("version subcommand shouldn't fail: %+v", err)
50+
}
51+
}

config/config.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"github.com/bpineau/katafygio/pkg/client"
88

99
"github.com/sirupsen/logrus"
10-
"k8s.io/client-go/rest"
1110
)
1211

1312
// KfConfig holds the configuration options passed at launch time (and the rest client)
@@ -22,7 +21,7 @@ type KfConfig struct {
2221
Logger *logrus.Logger
2322

2423
// Client represents a connection to a Kubernetes cluster
25-
Client *rest.Config
24+
Client client.Interface
2625

2726
// GitURL is the address of a remote git repository
2827
GitURL string
@@ -48,9 +47,9 @@ type KfConfig struct {
4847

4948
// Init initialize the config
5049
func (c *KfConfig) Init(apiserver string, kubeconfig string) (err error) {
51-
c.Client, err = client.NewRestConfig(apiserver, kubeconfig)
50+
c.Client, err = client.New(apiserver, kubeconfig)
5251
if err != nil {
53-
return fmt.Errorf("Failed init Kubernetes clientset: %+v", err)
52+
return fmt.Errorf("Failed init Kubernetes client: %+v", err)
5453
}
5554
return nil
5655
}

pkg/client/client.go

+31-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
// Package client initialize a Kubernete's client-go rest.Config or clientset
1+
// Package client initialize and wrap a Kubernete's client-go rest.Config client
22
package client
33

44
import (
5+
"fmt"
56
"os"
67
"path/filepath"
78

8-
"k8s.io/client-go/kubernetes"
99
"k8s.io/client-go/rest"
1010
"k8s.io/client-go/tools/clientcmd"
1111
"k8s.io/client-go/util/homedir"
@@ -14,13 +14,40 @@ import (
1414
_ "k8s.io/client-go/plugin/pkg/client/auth"
1515
)
1616

17-
// NewRestConfig create a *rest.Config, trying to mimic kubectl behavior:
17+
// Interface abstracts access to a concrete Kubernetes rest.Client
18+
type Interface interface {
19+
GetRestConfig() *rest.Config
20+
}
21+
22+
// RestClient holds a Kubernetes rest client configuration
23+
type RestClient struct {
24+
cfg *rest.Config
25+
}
26+
27+
// New create a new RestClient
28+
func New(apiserver, kubeconfig string) (*RestClient, error) {
29+
cfg, err := newRestConfig(apiserver, kubeconfig)
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to build a restconfig: %v", err)
32+
}
33+
34+
return &RestClient{
35+
cfg: cfg,
36+
}, nil
37+
}
38+
39+
// GetRestConfig returns the current rest.Config
40+
func (r *RestClient) GetRestConfig() *rest.Config {
41+
return r.cfg
42+
}
43+
44+
// newRestConfig create a *rest.Config, trying to mimic kubectl behavior:
1845
// - Explicit user provided api-server (and/or kubeconfig path) have higher priorities
1946
// - Else, use the config file path in KUBECONFIG environment variable (if any)
2047
// - Else, use the config file in ~/.kube/config, if any
2148
// - Else, consider we're running in cluster (in a pod), and use the pod's service
2249
// account and cluster's kubernetes.default service.
23-
func NewRestConfig(apiserver string, kubeconfig string) (*rest.Config, error) {
50+
func newRestConfig(apiserver string, kubeconfig string) (*rest.Config, error) {
2451
// if not passed as an argument, kubeconfig can be provided as env var
2552
if kubeconfig == "" {
2653
kubeconfig = os.Getenv("KUBECONFIG")
@@ -43,13 +70,3 @@ func NewRestConfig(apiserver string, kubeconfig string) (*rest.Config, error) {
4370
// else assume we're running in a pod, in cluster
4471
return rest.InClusterConfig()
4572
}
46-
47-
// NewClientSet create a clientset (a client connection to a Kubernetes cluster).
48-
func NewClientSet(apiserver string, kubeconfig string) (*kubernetes.Clientset, error) {
49-
config, err := NewRestConfig(apiserver, kubeconfig)
50-
if err != nil {
51-
return nil, err
52-
}
53-
54-
return kubernetes.NewForConfig(config)
55-
}

pkg/client/client_test.go

+11-11
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,33 @@ import (
66
"testing"
77
)
88

9-
const nonExistentPath = "\\/hopefully/non/existent/path"
9+
const nonExistentPath = "\\/non / existent / $path$"
1010

1111
func TestClientSet(t *testing.T) {
1212
here, _ := os.Getwd()
1313
_ = os.Setenv("HOME", here+"/../../assets")
14-
cs, err := NewClientSet("", "")
14+
cs, err := New("", "")
1515
if err != nil {
1616
t.Fatal(err)
1717
}
18-
if fmt.Sprintf("%T", cs) != "*kubernetes.Clientset" {
19-
t.Errorf("NewClientSet() didn't return a *kubernetes.Clientset: %T", cs)
18+
if fmt.Sprintf("%T", cs.GetRestConfig()) != "*rest.Config" {
19+
t.Errorf("GetRestConfig() didn't return a *rest.Config: %T", cs)
2020
}
2121

22-
cs, _ = NewClientSet("http://127.0.0.1", "/dev/null")
23-
if fmt.Sprintf("%T", cs) != "*kubernetes.Clientset" {
24-
t.Errorf("NewClientSet(server) didn't return a *kubernetes.Clientset: %T", cs)
22+
cs, _ = New("http://127.0.0.1", "/dev/null")
23+
if fmt.Sprintf("%T", cs.GetRestConfig()) != "*rest.Config" {
24+
t.Errorf("New(server) didn't return a *rest.Config: %T", cs)
2525
}
2626

27-
_, err = NewClientSet("http://127.0.0.1", nonExistentPath)
27+
_, err = New("http://127.0.0.1", nonExistentPath)
2828
if err == nil {
29-
t.Fatal("NewClientSet() should fail on non existent kubeconfig path")
29+
t.Fatal("New() should fail on non existent kubeconfig path")
3030
}
3131

3232
_ = os.Unsetenv("KUBERNETES_SERVICE_HOST")
3333
_ = os.Setenv("HOME", nonExistentPath)
34-
_, err = NewClientSet("", "")
34+
_, err = New("", "")
3535
if err == nil {
36-
t.Fatal("NewClientSet() should fail to load InClusterConfig without kube address env")
36+
t.Fatal("New() should fail to load InClusterConfig without kube address env")
3737
}
3838
}

pkg/observer/observer.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ func New(config *config.KfConfig, notif event.Notifier, factory ControllerFactor
5656
return &Observer{
5757
config: config,
5858
notifier: notif,
59-
discovery: discovery.NewDiscoveryClientForConfigOrDie(config.Client),
60-
cpool: dynamic.NewDynamicClientPool(config.Client),
59+
discovery: discovery.NewDiscoveryClientForConfigOrDie(config.Client.GetRestConfig()),
60+
cpool: dynamic.NewDynamicClientPool(config.Client.GetRestConfig()),
6161
ctrls: make(controllerCollection),
6262
factory: factory,
6363
}

pkg/observer/observer_test.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ func (m *mockFactory) NewController(client cache.ListerWatcher, notifier event.N
4646
return &mockCtrl{}
4747
}
4848

49+
type mockClient struct{}
50+
51+
func (m *mockClient) GetRestConfig() *restclient.Config {
52+
return &restclient.Config{}
53+
}
54+
4955
var stdVerbs = []string{"list", "get", "watch"}
5056
var emptyExclude = make([]string, 0)
5157

@@ -171,7 +177,7 @@ var resourcesTests = []resTest{
171177
func TestObserver(t *testing.T) {
172178
for _, tt := range resourcesTests {
173179
conf := &config.KfConfig{
174-
Client: &restclient.Config{},
180+
Client: new(mockClient),
175181
Logger: log.New("info", "", "test"),
176182
ExcludeKind: tt.exclude,
177183
}
@@ -212,7 +218,7 @@ var duplicatesTest = []*metav1.APIResourceList{
212218

213219
func TestObserverDuplicas(t *testing.T) {
214220
conf := &config.KfConfig{
215-
Client: &restclient.Config{},
221+
Client: new(mockClient),
216222
Logger: log.New("info", "", "test"),
217223
ExcludeKind: make([]string, 0),
218224
ExcludeObject: make([]string, 0),
@@ -242,7 +248,7 @@ func TestObserverDuplicas(t *testing.T) {
242248

243249
func TestObserverRecoverFromDicoveryFailure(t *testing.T) {
244250
conf := &config.KfConfig{
245-
Client: &restclient.Config{},
251+
Client: new(mockClient),
246252
Logger: log.New("info", "", "test"),
247253
ExcludeKind: make([]string, 0),
248254
ExcludeObject: make([]string, 0),

0 commit comments

Comments
 (0)