Skip to content
This repository has been archived by the owner on Oct 30, 2024. It is now read-only.

✨ Use dynamic kubeclient #433

Merged
merged 4 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 107 additions & 272 deletions internal/k8sinternal/client.go

Large diffs are not rendered by default.

97 changes: 66 additions & 31 deletions internal/k8sinternal/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
fakediscovery "k8s.io/client-go/discovery/fake"
fakedynamic "k8s.io/client-go/dynamic/fake"
fakeclientset "k8s.io/client-go/kubernetes/fake"
_ "k8s.io/client-go/plugin/pkg/client/auth/azure" // auth for AKS clusters
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // auth for GKE clusters
Expand Down Expand Up @@ -46,14 +51,14 @@ func TestKubeClientConfigCluster(t *testing.T) {
client := &MockK8sClient{}
var config *rest.Config = nil
client.On("InClusterConfig").Return(config, errors.New("mock error"))
clientset, err := k8sinternal.NewKubeClientCluster(client)
assert.Nil(clientset)
kubeclient, err := k8sinternal.NewKubeClientCluster(client)
assert.Nil(kubeclient)
assert.NotNil(err)

client = &MockK8sClient{}
client.On("InClusterConfig").Return(&rest.Config{}, nil)
clientset, err = k8sinternal.NewKubeClientCluster(client)
assert.NotNil(clientset)
kubeclient, err = k8sinternal.NewKubeClientCluster(client)
assert.NotNil(kubeclient)
assert.NoError(err)
}

Expand Down Expand Up @@ -96,45 +101,35 @@ func TestGetAllResources(t *testing.T) {
}
}

clientset := fakeclientset.NewSimpleClientset(resources...)
assert.Len(t, k8sinternal.GetAllResources(clientset, k8sinternal.ClientOptions{}), len(resourceTemplates)*len(namespaces))

// Because field selectors are handled server-side, the fake clientset does not support them
// which means the Namespace resources don't get filtered (this is not a problem when using
// a real clientset)
// See https://github.com/kubernetes/client-go/issues/326
client := newFakeKubeClient(resources...)
assert.Len(t, client.GetAllResources(k8sinternal.ClientOptions{}), len(resourceTemplates)*len(namespaces))
assert.Len(
t,
k8sinternal.GetAllResources(clientset, k8sinternal.ClientOptions{Namespace: namespaces[0]}),
len(resourceTemplates)+(len(namespaces)-1),
client.GetAllResources(k8sinternal.ClientOptions{Namespace: namespaces[0]}),
len(resourceTemplates),
)
}

func setNamespace(resource k8s.Resource, namespace string) {
if _, ok := resource.(*k8s.NamespaceV1); ok {
k8s.GetObjectMeta(resource).Name = namespace
k8s.GetObjectMeta(resource).SetName(namespace)
} else {
k8s.GetObjectMeta(resource).Namespace = namespace
k8s.GetObjectMeta(resource).SetNamespace(namespace)
}
}

func TestGetKubernetesVersion(t *testing.T) {
client := fakeclientset.NewSimpleClientset()
fakeDiscovery, ok := client.Discovery().(*fakediscovery.FakeDiscovery)
if !ok {
t.Fatalf("couldn't mock server version")
}

fakeDiscovery.FakedServerVersion = &version.Info{
serverVersion := &version.Info{
Major: "0",
Minor: "0",
GitCommit: "0000",
Platform: "ACME 8-bit",
}

r, err := k8sinternal.GetKubernetesVersion(client)
client := newFakeKubeClientWithServerVersion(serverVersion)
r, err := client.GetKubernetesVersion()
assert.Nil(t, err)
assert.EqualValues(t, *fakeDiscovery.FakedServerVersion, *r)
assert.EqualValues(t, *serverVersion, *r)
}

func TestIncludeGenerated(t *testing.T) {
Expand All @@ -148,26 +143,23 @@ func TestIncludeGenerated(t *testing.T) {
test.CreateNamespace(t, namespace)
test.ApplyManifest(t, "./fixtures/include-generated.yml", namespace)

clientset, err := k8sinternal.NewKubeClientLocal("")
client, err := k8sinternal.NewKubeClientLocal("")
require.NoError(t, err)

// Test IncludeGenerated = false
resources := k8sinternal.GetAllResources(
clientset,
resources := client.GetAllResources(
k8sinternal.ClientOptions{Namespace: namespace, IncludeGenerated: false},
)
assert.False(t, hasPod(resources), "Expected no pods for IncludeGenerated=false")

// Test IncludeGenerated unspecified defaults to false
resources = k8sinternal.GetAllResources(
clientset,
resources = client.GetAllResources(
k8sinternal.ClientOptions{Namespace: namespace},
)
assert.False(t, hasPod(resources), "Expected no pods if IncludeGenerated is unspecified (ie. default to false)")

// Test IncludeGenerated = true
resources = k8sinternal.GetAllResources(
clientset,
resources = client.GetAllResources(
k8sinternal.ClientOptions{Namespace: namespace, IncludeGenerated: true},
)
assert.True(t, hasPod(resources), "Expected pods for IncludeGenerated=true")
Expand All @@ -181,3 +173,46 @@ func hasPod(resources []k8s.Resource) bool {
}
return false
}

func newFakeKubeClient(resources ...runtime.Object) k8sinternal.KubeClient {
return newFakeKubeClientWithServerVersion(nil, resources...)
}

func newFakeKubeClientWithServerVersion(serverversion *version.Info, resources ...runtime.Object) k8sinternal.KubeClient {
clientset := fakeclientset.NewSimpleClientset()
fakeDiscovery, _ := clientset.Discovery().(*fakediscovery.FakeDiscovery)
if serverversion != nil {
fakeDiscovery.FakedServerVersion = serverversion
}
unstructuredresources := []runtime.Object{}
gvrToListKind := map[schema.GroupVersionResource]string{}
gvAPIResources := map[string][]metav1.APIResource{}
for _, r := range resources {
gvk := r.GetObjectKind().GroupVersionKind()
listGVK := gvk
listGVK.Kind += "List"

u := unstructured.Unstructured{}
u.SetGroupVersionKind(r.GetObjectKind().GroupVersionKind())
u.SetName(k8s.GetObjectMeta(r).GetName())
u.SetNamespace(k8s.GetObjectMeta(r).GetNamespace())
unstructuredresources = (append(unstructuredresources, &u))

kind := r.GetObjectKind().GroupVersionKind().Kind
plural, _ := meta.UnsafeGuessKindToResource(r.GetObjectKind().GroupVersionKind())
apiresource := metav1.APIResource{Name: plural.Resource, Namespaced: false, Group: gvk.Group, Version: gvk.Version, Kind: kind, Verbs: metav1.Verbs{"list"}}
gvr := schema.GroupVersionResource{Group: apiresource.Group, Version: apiresource.Version, Resource: apiresource.Name}
if _, ok := gvrToListKind[gvr]; !ok {
gvrToListKind[gvr] = kind + "List"
gv := gvk.GroupVersion().String()
gvAPIResources[gv] = append(gvAPIResources[gv], apiresource)
}
}
for gv, apiresources := range gvAPIResources {
fakeDiscovery.Resources = append(fakeDiscovery.Resources, &metav1.APIResourceList{
GroupVersion: gv,
APIResources: apiresources})
}
fakedynamic := fakedynamic.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, unstructuredresources...)
return k8sinternal.NewKubeClient(fakedynamic, fakeDiscovery)
}
18 changes: 6 additions & 12 deletions internal/k8sinternal/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ func TestEncodeDecode(t *testing.T) {

func TestGetContainers(t *testing.T) {
for _, resource := range getAllResources(t) {
if !k8s.IsSupportedResourceType(resource) {
continue
}
containers := k8s.GetContainers(resource)
switch resource.(type) {
case *k8s.NamespaceV1:
Expand Down Expand Up @@ -80,25 +77,22 @@ func TestGetObjectMeta(t *testing.T) {
deployment := k8s.NewDeployment()
deployment.ObjectMeta = objectMeta
deployment.Spec.Template.ObjectMeta = podObjectMeta
assert.Equal(objectMeta, *k8s.GetObjectMeta(deployment))
assert.Equal(podObjectMeta, *k8s.GetPodObjectMeta(deployment))
assert.Equal(&objectMeta, k8s.GetObjectMeta(deployment))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: this looks functionally equivalent, is there an advantage to doing it this way?

Copy link
Contributor Author

@jerr jerr Jun 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is because the k8s.GetObjectMeta function returns the metav1.Object interface instead of a metav1.ObjectMeta struct reference.

It is provided by the ObjectMetaAccessor.GetObjectMeta() function.

// GetObjectMeta returns the highest-level ObjectMeta
func GetObjectMeta(resource Resource) metav1.Object {
obj, _ := resource.(metav1.ObjectMetaAccessor)
if obj != nil {
return obj.GetObjectMeta()
}
return nil
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, missed the type switch and thought it was some go convention I didn't know about 😄

assert.Equal(&podObjectMeta, k8s.GetPodObjectMeta(deployment))

pod := k8s.NewPod()
pod.ObjectMeta = objectMeta
assert.Equal(objectMeta, *k8s.GetObjectMeta(pod))
assert.Equal(objectMeta, *k8s.GetPodObjectMeta(pod))
assert.Equal(&objectMeta, k8s.GetObjectMeta(pod))
assert.Equal(&objectMeta, k8s.GetPodObjectMeta(pod))

namespace := k8s.NewNamespace()
namespace.ObjectMeta = objectMeta
assert.Equal(objectMeta, *k8s.GetObjectMeta(namespace))
assert.Equal(objectMeta, *k8s.GetPodObjectMeta(namespace))
assert.Equal(&objectMeta, k8s.GetObjectMeta(namespace))
assert.Equal(&objectMeta, k8s.GetPodObjectMeta(namespace))
}

func TestGetPodTemplateSpec(t *testing.T) {
for _, resource := range getAllResources(t) {
if !k8s.IsSupportedResourceType(resource) {
continue
}
podTemplateSpec := k8s.GetPodTemplateSpec(resource)
switch resource.(type) {
case *k8s.PodV1, *k8s.NamespaceV1:
Expand Down
8 changes: 4 additions & 4 deletions kubeaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ func (a *Kubeaudit) AuditCluster(options AuditOptions) (*Report, error) {
return nil, errors.New("failed to audit resources in cluster mode: not running in cluster")
}

clientset, err := k8sinternal.NewKubeClientCluster(k8sinternal.DefaultClient)
client, err := k8sinternal.NewKubeClientCluster(k8sinternal.DefaultClient)
if err != nil {
return nil, err
}

resources := getResourcesFromClientset(clientset, options)
resources := getResourcesFromClient(client, options)
results, err := auditResources(resources, a.auditors)
if err != nil {
return nil, err
Expand All @@ -183,14 +183,14 @@ func (a *Kubeaudit) AuditCluster(options AuditOptions) (*Report, error) {

// AuditLocal audits the Kubernetes resources found in the provided Kubernetes config file
func (a *Kubeaudit) AuditLocal(configpath string, options AuditOptions) (*Report, error) {
clientset, err := k8sinternal.NewKubeClientLocal(configpath)
client, err := k8sinternal.NewKubeClientLocal(configpath)
if err == k8sinternal.ErrNoReadableKubeConfig {
return nil, fmt.Errorf("failed to open kubeconfig file %s", configpath)
} else if err != nil {
return nil, err
}

resources := getResourcesFromClientset(clientset, options)
resources := getResourcesFromClient(client, options)
results, err := auditResources(resources, a.auditors)
if err != nil {
return nil, err
Expand Down
37 changes: 9 additions & 28 deletions pkg/k8s/helpers.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package k8s

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// NewTrue returns a pointer to a boolean variable set to true
func NewTrue() *bool {
b := true
Expand Down Expand Up @@ -64,40 +68,17 @@ func GetLabels(resource Resource) map[string]string {
}

// GetObjectMeta returns the highest-level ObjectMeta
func GetObjectMeta(resource Resource) *ObjectMetaV1 {
switch kubeType := resource.(type) {
case *CronJobV1Beta1:
return &kubeType.ObjectMeta
case *DaemonSetV1:
return &kubeType.ObjectMeta
case *DeploymentV1:
return &kubeType.ObjectMeta
case *JobV1:
return &kubeType.ObjectMeta
case *PodTemplateV1:
return &kubeType.ObjectMeta
case *ReplicationControllerV1:
return &kubeType.ObjectMeta
case *StatefulSetV1:
return &kubeType.ObjectMeta
case *PodV1:
return &kubeType.ObjectMeta
case *NamespaceV1:
return &kubeType.ObjectMeta
case *NetworkPolicyV1:
return &kubeType.ObjectMeta
case *ServiceAccountV1:
return &kubeType.ObjectMeta
case *ServiceV1:
return &kubeType.ObjectMeta
func GetObjectMeta(resource Resource) metav1.Object {
obj, _ := resource.(metav1.ObjectMetaAccessor)
if obj != nil {
return obj.GetObjectMeta()
}

return nil
}

// GetPodObjectMeta returns the ObjectMeta at the pod level. If the resource does not have pods, then it returns
// the highest-level ObjectMeta
func GetPodObjectMeta(resource Resource) *ObjectMetaV1 {
func GetPodObjectMeta(resource Resource) metav1.Object {
podTemplateSpec := GetPodTemplateSpec(resource)
if podTemplateSpec != nil {
return &podTemplateSpec.ObjectMeta
Expand Down
2 changes: 1 addition & 1 deletion pkg/k8s/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func NewNamespace() *NamespaceV1 {
func NewDaemonSet() *DaemonSetV1 {
return &DaemonSetV1{
TypeMeta: TypeMetaV1{
Kind: "Daemonset",
Kind: "DaemonSet",
APIVersion: "apps/v1",
},
ObjectMeta: ObjectMetaV1{},
Expand Down
21 changes: 0 additions & 21 deletions pkg/k8s/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,24 +111,3 @@ type TypeMetaV1 = metav1.TypeMeta

// UnsupportedType is a type alias for v1 version of the k8s apps API, this is meant for testing
type UnsupportedType = apiv1.Binding

// IsSupportedResourceType returns true if obj is a supported Kubernetes resource type
func IsSupportedResourceType(obj Resource) bool {
switch obj.(type) {
case *CronJobV1Beta1,
*DaemonSetV1,
*DeploymentV1,
*JobV1,
*NamespaceV1,
*NetworkPolicyV1,
*PodV1,
*PodTemplateV1,
*ReplicationControllerV1,
*ServiceAccountV1,
*ServiceV1,
*StatefulSetV1:
return true
default:
return false
}
}
15 changes: 2 additions & 13 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import (
"github.com/Shopify/kubeaudit/internal/k8sinternal"
"github.com/Shopify/kubeaudit/pkg/k8s"
"gopkg.in/yaml.v3"
"k8s.io/client-go/kubernetes"
)

func getResourcesFromClientset(clientset kubernetes.Interface, options k8sinternal.ClientOptions) []KubeResource {
func getResourcesFromClient(client k8sinternal.KubeClient, options k8sinternal.ClientOptions) []KubeResource {
var resources []KubeResource

for _, resource := range k8sinternal.GetAllResources(clientset, options) {
for _, resource := range client.GetAllResources(options) {
resources = append(resources, &kubeResource{object: resource})
}

Expand Down Expand Up @@ -66,16 +65,6 @@ func auditResource(resource KubeResource, resources []KubeResource, auditables [
return result, nil
}

if !k8s.IsSupportedResourceType(resource.Object()) {
auditResult := &AuditResult{
Name: ErrorUnsupportedResource,
Severity: Warn,
Message: "Resource is not currently supported.",
}
result.AuditResults = append(result.AuditResults, auditResult)
return result, nil
}

for _, auditable := range auditables {
auditResults, err := auditable.Audit(resource.Object(), unwrapResources(resources))
if err != nil {
Expand Down
Loading