Skip to content

Commit

Permalink
Merge pull request #85 from toddtreece/main
Browse files Browse the repository at this point in the history
Use REST resource mapper
  • Loading branch information
javaducky authored Jan 12, 2023
2 parents d629e50 + 42b72ec commit a90ee52
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 48 deletions.
14 changes: 7 additions & 7 deletions examples/ingress_operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ export default function () {
})

describe('Retrieve all available ingresses', () => {
expect(kubernetes.list("Ingress", ns).length).to.be.at.least(1)
expect(kubernetes.list("Ingress.networking.k8s.io", ns).length).to.be.at.least(1)
})

describe('Retrieve our ingress by name and namespace', () => {
let fetched = kubernetes.get("Ingress", name, ns)
let fetched = kubernetes.get("Ingress.networking.k8s.io", name, ns)
expect(ingress.metadata.uid).to.equal(fetched.metadata.uid)
})

Expand All @@ -81,12 +81,12 @@ export default function () {
json.spec.rules[0].http.paths[0].path = newValue

kubernetes.update(json)
let updated = kubernetes.get("Ingress", name, ns)
let updated = kubernetes.get("Ingress.networking.k8s.io", name, ns)
expect(updated.spec.rules[0].http.paths[0].path).to.be.equal(newValue)
})

describe('Remove our ingresses to cleanup', () => {
kubernetes.delete("Ingress", name, ns)
kubernetes.delete("Ingress.networking.k8s.io", name, ns)
})
})

Expand All @@ -98,19 +98,19 @@ export default function () {

describe('Create our ingress using the YAML definition', () => {
kubernetes.apply(yaml)
let created = kubernetes.get("Ingress", name, ns)
let created = kubernetes.get("Ingress.networking.k8s.io", name, ns)
expect(created.metadata).to.have.property('uid')
uid = created.metadata.uid
})

describe('Update our ingress with a modified YAML definition', () => {
kubernetes.apply(yaml)
let updated = kubernetes.get("Ingress", name, ns)
let updated = kubernetes.get("Ingress.networking.k8s.io", name, ns)
expect(updated.metadata.uid).to.be.equal(uid)
})

describe('Remove our ingresses to cleanup', () => {
kubernetes.delete("Ingress", name, ns)
kubernetes.delete("Ingress.networking.k8s.io", name, ns)
})
})

Expand Down
41 changes: 41 additions & 0 deletions internal/testutils/fake.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package testutils

import (
"fmt"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
)
Expand All @@ -16,3 +20,40 @@ func NewFakeDynamic(objs ...runtime.Object) (*dynamicfake.FakeDynamicClient, err

return dynamicfake.NewSimpleDynamicClient(scheme, objs...), nil
}

type FakeRESTMapper struct {
meta.RESTMapper
}

func (f *FakeRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
kindMapping := map[string]schema.GroupVersionResource{
"ConfigMap": {Group: "", Version: "v1", Resource: "configmaps"},
"Deployment": {Group: "apps", Version: "v1", Resource: "deployments"},
"Endpoint": {Group: "", Version: "v1", Resource: "endpoints"},
"Ingress": {Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"},
"Job": {Group: "batch", Version: "v1", Resource: "jobs"},
"PersistentVolume": {Group: "", Version: "v1", Resource: "persistentvolumes"},
"PersistentVolumeClaim": {Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
"Pod": {Group: "", Version: "v1", Resource: "pods"},
"Namespace": {Group: "", Version: "v1", Resource: "namespaces"},
"Node": {Group: "", Version: "v1", Resource: "nodes"},
"Secret": {Group: "", Version: "v1", Resource: "secrets"},
"Service": {Group: "", Version: "v1", Resource: "services"},
"StatefulSet": {Group: "apps", Version: "v1", Resource: "statefulsets"},
}

gvr, found := kindMapping[gk.Kind]
if !found {
return nil, fmt.Errorf("unknown kind: '%s'", gk.Kind)
}
scope := meta.RESTScopeNamespace
if gk.Kind == "Namespace" {
scope = meta.RESTScopeRoot
}

return &meta.RESTMapping{
Resource: gvr,
GroupVersionKind: gvr.GroupVersion().WithKind(gk.Kind),
Scope: scope,
}, nil
}
6 changes: 5 additions & 1 deletion kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/grafana/xk6-kubernetes/pkg/services"

"go.k6.io/k6/js/modules"
"k8s.io/apimachinery/pkg/api/meta"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
Expand All @@ -48,6 +49,8 @@ type ModuleInstance struct {
clientset kubernetes.Interface
// dynamic enables injection of a fake dynamic client for unit tests
dynamic dynamic.Interface
// mapper enables injection of a fake RESTMapper for unit tests
mapper meta.RESTMapper
}

// Kubernetes is the exported object used within JavaScript.
Expand Down Expand Up @@ -141,10 +144,11 @@ func (mi *ModuleInstance) newClient(c goja.ConstructorCall) *goja.Object {
}
obj.Kubernetes = k8s
} else {
// Pre-configured dynamic client is injected for unit testing
// Pre-configured dynamic client and RESTMapper are injected for unit testing
k8s, err := api.NewFromConfig(
api.KubernetesConfig{
Client: mi.dynamic,
Mapper: mi.mapper,
Context: ctx,
},
)
Expand Down
1 change: 1 addition & 0 deletions kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func setupTestEnv(t *testing.T, objs ...runtime.Object) *goja.Runtime {
t.Errorf("unexpected error creating fake client %v", err)
}
m.dynamic = dynamic
m.mapper = &localutils.FakeRESTMapper{}

return rt
}
Expand Down
24 changes: 22 additions & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import (
"github.com/grafana/xk6-kubernetes/pkg/helpers"
"github.com/grafana/xk6-kubernetes/pkg/resources"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
)

// Kubernetes defines an interface that extends kubernetes interface[k8s.io/client-go/kubernetes.Interface] adding
Expand All @@ -28,32 +32,48 @@ type KubernetesConfig struct {
Config *rest.Config
// Client is a pre-configured dynamic client. If provided, the rest config is not used
Client dynamic.Interface
// Mapper is a pre-configured RESTMapper. If provided, the rest config is not used
Mapper meta.RESTMapper
}

// kubernetes holds references to implementation of the Kubernetes interface
type kubernetes struct {
ctx context.Context
*resources.Client
*restmapper.DeferredDiscoveryRESTMapper
}

// NewFromConfig returns a Kubernetes instance
func NewFromConfig(c KubernetesConfig) (Kubernetes, error) {
var (
err error
discoveryClient *discovery.DiscoveryClient
)

ctx := c.Context
if ctx == nil {
ctx = context.TODO()
}

var client *resources.Client
var err error
if c.Client != nil {
client = resources.NewFromClient(ctx, c.Client)
client = resources.NewFromClient(ctx, c.Client).WithMapper(c.Mapper)
} else {
client, err = resources.NewFromConfig(ctx, c.Config)
if err != nil {
return nil, err
}
}

if c.Mapper == nil {
discoveryClient, err = discovery.NewDiscoveryClientForConfig(c.Config)
mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient))
if err != nil {
return nil, err
}
client.WithMapper(mapper)
}

return &kubernetes{
ctx: ctx,
Client: client,
Expand Down
2 changes: 1 addition & 1 deletion pkg/helpers/pods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestPods_Wait(t *testing.T) {
t.Run(tc.test, func(t *testing.T) {
t.Parallel()
fake, _ := testutils.NewFakeDynamic()
client := resources.NewFromClient(context.TODO(), fake)
client := resources.NewFromClient(context.TODO(), fake).WithMapper(&testutils.FakeRESTMapper{})
h := NewHelper(context.TODO(), client, testNamespace)
pod := buildPod()
_, err := client.Structured().Create(pod)
Expand Down
4 changes: 2 additions & 2 deletions pkg/helpers/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func Test_WaitServiceReady(t *testing.T) {
objs = append(objs, tc.endpoints)
}
fake, _ := testutils.NewFakeDynamic(objs...)
client := resources.NewFromClient(context.TODO(), fake)
client := resources.NewFromClient(context.TODO(), fake).WithMapper(&testutils.FakeRESTMapper{})
h := NewHelper(context.TODO(), client, "default")

go func(tc TestCase) {
Expand Down Expand Up @@ -265,7 +265,7 @@ func Test_GetServiceIP(t *testing.T) {
t.Parallel()

fake, _ := testutils.NewFakeDynamic()
client := resources.NewFromClient(context.TODO(), fake)
client := resources.NewFromClient(context.TODO(), fake).WithMapper(&testutils.FakeRESTMapper{})
h := NewHelper(context.TODO(), client, "default")

svc := buildService()
Expand Down
52 changes: 24 additions & 28 deletions pkg/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/grafana/xk6-kubernetes/pkg/utils"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -55,6 +56,7 @@ type structured struct {
type Client struct {
ctx context.Context
dynamic dynamic.Interface
mapper meta.RESTMapper
serializer runtime.Serializer
}

Expand All @@ -77,34 +79,28 @@ func NewFromClient(ctx context.Context, dynamic dynamic.Interface) *Client {
}
}

func (c *Client) WithMapper(mapper meta.RESTMapper) *Client {
c.mapper = mapper
return c
}

// getResource maps kinds to api resources
func (c *Client) getResource(kind string, namespace string) (dynamic.ResourceInterface, error) {
kindMapping := map[string]schema.GroupVersionResource{
"ConfigMap": {Group: "", Version: "v1", Resource: "configmaps"},
"Deployment": {Group: "apps", Version: "v1", Resource: "deployments"},
"Endpoint": {Group: "", Version: "v1", Resource: "endpoints"},
"Ingress": {Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"},
"Job": {Group: "batch", Version: "v1", Resource: "jobs"},
"PersistentVolume": {Group: "", Version: "v1", Resource: "persistentvolumes"},
"PersistentVolumeClaim": {Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
"Pod": {Group: "", Version: "v1", Resource: "pods"},
"Namespace": {Group: "", Version: "v1", Resource: "namespaces"},
"Node": {Group: "", Version: "v1", Resource: "nodes"},
"Secret": {Group: "", Version: "v1", Resource: "secrets"},
"Service": {Group: "", Version: "v1", Resource: "services"},
"StatefulSet": {Group: "apps", Version: "v1", Resource: "statefulsets"},
}

gvr, found := kindMapping[kind]
if !found {
return nil, fmt.Errorf("unknown kind: '%s'", kind)
func (c *Client) getResource(kind string, namespace string, versions ...string) (dynamic.ResourceInterface, error) {
gk := schema.ParseGroupKind(kind)
if c.mapper == nil {
return nil, fmt.Errorf("RESTMapper not initialized")
}

mapping, err := c.mapper.RESTMapping(gk, versions...)
if err != nil {
return nil, err
}

var resource dynamic.ResourceInterface
if kind == "Namespace" {
resource = c.dynamic.Resource(gvr)
if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
resource = c.dynamic.Resource(mapping.Resource).Namespace(namespace)
} else {
resource = c.dynamic.Resource(gvr).Namespace(namespace)
resource = c.dynamic.Resource(mapping.Resource)
}

return resource, nil
Expand All @@ -115,7 +111,7 @@ func (c *Client) Apply(manifest string) error {
uObj := &unstructured.Unstructured{}
_, gvk, err := c.serializer.Decode([]byte(manifest), nil, uObj)
if err != nil {
return err
return fmt.Errorf("failed to decode manifest: %w", err)
}

name := uObj.GetName()
Expand All @@ -124,9 +120,9 @@ func (c *Client) Apply(manifest string) error {
namespace = "default"
}

resource, err := c.getResource(gvk.Kind, namespace)
resource, err := c.getResource(gvk.GroupKind().String(), namespace, gvk.Version)
if err != nil {
return err
return fmt.Errorf("failed to get resource: %w", err)
}

_, err = resource.Apply(
Expand All @@ -152,7 +148,7 @@ func (c *Client) Create(obj map[string]interface{}) (map[string]interface{}, err
namespace = "default"
}

resource, err := c.getResource(gvk.Kind, namespace)
resource, err := c.getResource(gvk.GroupKind().String(), namespace)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -227,7 +223,7 @@ func (c *Client) Update(obj map[string]interface{}) (map[string]interface{}, err
if namespace == "" {
namespace = "default"
}
resource, err := c.getResource(gvk.Kind, namespace)
resource, err := c.getResource(gvk.GroupKind().String(), namespace)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit a90ee52

Please sign in to comment.