Skip to content

Commit

Permalink
Auth agnostic kube client
Browse files Browse the repository at this point in the history
Wrapping around the the kubectl interface to make the client API
agnostic from the authentication mechanism. Also adding support for an
internal schema that avoids having to indicate the resource type because
it's inferred from the concrete CRD type.

This allows to provide a common interface that can be implemented by
both a kubectl binary wrapper and a controller runtime client. With
this, we will be be able to build logic that interacts with the api
server and that can be shared between the CLI and the controller.
  • Loading branch information
g-gaston committed Apr 28, 2022
1 parent b3e58c4 commit 8f73688
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 12 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ mocks: ## Generate mocks
${GOPATH}/bin/mockgen -destination=pkg/curatedpackages/mocks/kubectlrunner.go -package=mocks -source "pkg/curatedpackages/kubectlrunner.go" KubectlRunner
${GOPATH}/bin/mockgen -destination=pkg/curatedpackages/mocks/reader.go -package=mocks -source "pkg/curatedpackages/bundle.go" Reader BundleRegistry
${GOPATH}/bin/mockgen -destination=pkg/curatedpackages/mocks/bundlemanager.go -package=mocks -source "pkg/curatedpackages/bundlemanager.go" Manager
${GOPATH}/bin/mockgen -destination=pkg/clients/kubernetes/mocks/kubectl.go -package=mocks -source "pkg/clients/kubernetes/unauth.go"

.PHONY: verify-mocks
verify-mocks: mocks ## Verify if mocks need to be updated
Expand Down
27 changes: 27 additions & 0 deletions pkg/clients/kubernetes/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kubernetes

import (
"context"

"k8s.io/apimachinery/pkg/runtime"
)

// KubeconfigClient is an authenticated kubernetes API client
// it authenticates using the credentials of a kubeconfig file
type KubeconfigClient struct {
client *UnAuthClient
kubeconfig string
}

func NewKubeconfigClient(client *UnAuthClient, kubeconfig string) *KubeconfigClient {
return &KubeconfigClient{
client: client,
kubeconfig: kubeconfig,
}
}

// Get performs a GET call to the kube API server
// and unmarshalls the response into the provdied Object
func (c *KubeconfigClient) Get(ctx context.Context, name, namespace string, obj runtime.Object) error {
return c.client.Get(ctx, name, namespace, c.kubeconfig, obj)
}
34 changes: 34 additions & 0 deletions pkg/clients/kubernetes/kubeconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kubernetes_test

import (
"context"
"testing"

"github.com/golang/mock/gomock"
. "github.com/onsi/gomega"

anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
"github.com/aws/eks-anywhere/pkg/clients/kubernetes"
"github.com/aws/eks-anywhere/pkg/clients/kubernetes/mocks"
)

func TestKubeconfigClientGet(t *testing.T) {
g := NewWithT(t)
ctx := context.Background()
ctrl := gomock.NewController(t)
kubectl := mocks.NewMockKubectlGetter(ctrl)
kubeconfig := "k.kubeconfig"

name := "eksa cluster"
namespace := "eksa-system"
obj := &anywherev1.Cluster{}
wantResourceType := "Cluster.v1alpha1.anywhere.eks.amazonaws.com"

kubectl.EXPECT().GetObject(ctx, wantResourceType, name, namespace, kubeconfig, obj)

c := kubernetes.NewUnAuthClient(kubectl)
g.Expect(c.Init()).To(Succeed())
kc := c.KubeconfigClient(kubeconfig)

g.Expect(kc.Get(ctx, name, namespace, obj)).To(Succeed())
}
50 changes: 50 additions & 0 deletions pkg/clients/kubernetes/mocks/kubectl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions pkg/clients/kubernetes/scheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kubernetes

import (
"k8s.io/apimachinery/pkg/runtime"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"

anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
)

type schemeAdder func(s *runtime.Scheme) error

var schemeAdders = []schemeAdder{
clusterv1.AddToScheme,
anywherev1.AddToScheme,
}

func addToScheme(scheme *runtime.Scheme, schemeAdder ...schemeAdder) error {
for _, adder := range schemeAdders {
if err := adder(scheme); err != nil {
return err
}
}

return nil
}
56 changes: 56 additions & 0 deletions pkg/clients/kubernetes/unauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package kubernetes

import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

type KubectlGetter interface {
GetObject(ctx context.Context, resourceType, name, namespace, kubeconfig string, obj runtime.Object) error
}

// UnAuthClient is a generic kubernetes API client that takes a kubeconfig
// file on every call in order to authenticate
type UnAuthClient struct {
kubectl KubectlGetter
scheme *runtime.Scheme
}

func NewUnAuthClient(kubectl KubectlGetter) *UnAuthClient {
return &UnAuthClient{
kubectl: kubectl,
scheme: runtime.NewScheme(),
}
}

// Init initializes the client internal API scheme
// It has always be invoked at least once before making any API call
// It is not thread safe
func (c *UnAuthClient) Init() error {
return addToScheme(c.scheme, schemeAdders...)
}

// Get performs a GET call to the kube API server authenticating with a kubeconfig file
// and unmarshalls the response into the provdied Object
func (c *UnAuthClient) Get(ctx context.Context, name, namespace, kubeconfig string, obj runtime.Object) error {
groupVersionKind, err := apiutil.GVKForObject(obj, c.scheme)
if err != nil {
return fmt.Errorf("getting kubernetes resource: %v", err)
}

resourceType := groupVersionToKubectlResourceType(groupVersionKind)
return c.kubectl.GetObject(ctx, resourceType, name, namespace, kubeconfig, obj)
}

// KubeconfigClient returns an equivalent authenticated client
func (c *UnAuthClient) KubeconfigClient(kubeconfig string) *KubeconfigClient {
return NewKubeconfigClient(c, kubeconfig)
}

func groupVersionToKubectlResourceType(g schema.GroupVersionKind) string {
return fmt.Sprintf("%s.%s.%s", g.Kind, g.Version, g.Group)
}
66 changes: 66 additions & 0 deletions pkg/clients/kubernetes/unauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package kubernetes_test

import (
"context"
"testing"

"github.com/golang/mock/gomock"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
clusterapiv1 "sigs.k8s.io/cluster-api/api/v1beta1"

anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
"github.com/aws/eks-anywhere/pkg/clients/kubernetes"
"github.com/aws/eks-anywhere/pkg/clients/kubernetes/mocks"
releasev1 "github.com/aws/eks-anywhere/release/api/v1alpha1"
)

func TestUnAuthClientGetSuccess(t *testing.T) {
tests := []struct {
name string
namespace string
obj runtime.Object
wantResourceType string
}{
{
name: "eksa cluster",
namespace: "eksa-system",
obj: &anywherev1.Cluster{},
wantResourceType: "Cluster.v1alpha1.anywhere.eks.amazonaws.com",
},
{
name: "capi cluster",
namespace: "eksa-system",
obj: &clusterapiv1.Cluster{},
wantResourceType: "Cluster.v1beta1.cluster.x-k8s.io",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
ctx := context.Background()
ctrl := gomock.NewController(t)
kubectl := mocks.NewMockKubectlGetter(ctrl)
kubeconfig := "k.kubeconfig"

kubectl.EXPECT().GetObject(ctx, tt.wantResourceType, tt.name, tt.namespace, kubeconfig, tt.obj)

c := kubernetes.NewUnAuthClient(kubectl)
g.Expect(c.Init()).To(Succeed())

g.Expect(c.Get(ctx, tt.name, tt.namespace, kubeconfig, tt.obj)).To(Succeed())
})
}
}

func TestUnAuthClientGetUnknownObjType(t *testing.T) {
g := NewWithT(t)
ctx := context.Background()
ctrl := gomock.NewController(t)
kubectl := mocks.NewMockKubectlGetter(ctrl)

c := kubernetes.NewUnAuthClient(kubectl)
g.Expect(c.Init()).To(Succeed())

g.Expect(c.Get(ctx, "name", "namespace", "kubeconfig", &releasev1.Release{})).Error()
}
21 changes: 21 additions & 0 deletions pkg/dependencies/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/aws/eks-anywhere/pkg/awsiamauth"
"github.com/aws/eks-anywhere/pkg/bootstrapper"
"github.com/aws/eks-anywhere/pkg/clients/flux"
"github.com/aws/eks-anywhere/pkg/clients/kubernetes"
"github.com/aws/eks-anywhere/pkg/cluster"
"github.com/aws/eks-anywhere/pkg/clusterapi"
"github.com/aws/eks-anywhere/pkg/clustermanager"
Expand Down Expand Up @@ -57,6 +58,7 @@ type Dependencies struct {
Flux *executables.Flux
Troubleshoot *executables.Troubleshoot
Helm *executables.Helm
UnAuthKubeClient *kubernetes.UnAuthClient
Networking clustermanager.Networking
AwsIamAuth clustermanager.AwsIamAuth
ClusterManager *clustermanager.ClusterManager
Expand Down Expand Up @@ -900,3 +902,22 @@ func (f *Factory) WithManifestReader() *Factory {

return f
}

func (f *Factory) WithUnAuthKubeClient() *Factory {
f.WithKubectl()

f.buildSteps = append(f.buildSteps, func(ctx context.Context) error {
if f.dependencies.UnAuthKubeClient != nil {
return nil
}

f.dependencies.UnAuthKubeClient = kubernetes.NewUnAuthClient(f.dependencies.Kubectl)
if err := f.dependencies.UnAuthKubeClient.Init(); err != nil {
return fmt.Errorf("building unauth kube client: %v", err)
}

return nil
})

return f
}
2 changes: 2 additions & 0 deletions pkg/dependencies/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func TestFactoryBuildWithMultipleDependencies(t *testing.T) {
WithTroubleshoot().
WithCAPIManager().
WithManifestReader().
WithUnAuthKubeClient().
Build(context.Background())

tt.Expect(err).To(BeNil())
Expand All @@ -90,6 +91,7 @@ func TestFactoryBuildWithMultipleDependencies(t *testing.T) {
tt.Expect(deps.Troubleshoot).NotTo(BeNil())
tt.Expect(deps.CAPIManager).NotTo(BeNil())
tt.Expect(deps.ManifestReader).NotTo(BeNil())
tt.Expect(deps.UnAuthKubeClient).NotTo(BeNil())
}

func TestFactoryBuildWithRegistryMirror(t *testing.T) {
Expand Down
Loading

0 comments on commit 8f73688

Please sign in to comment.