Skip to content
This repository has been archived by the owner on Nov 19, 2020. It is now read-only.

list/watch resources across all namespaces #32

Merged
merged 1 commit into from
Feb 28, 2017
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
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,10 @@ This client supports every API group version present since 1.3.

### Namespaces

Clients are initialized with a default namespace. For in-cluster clients, this is the namespace the pod was deployed in.

```go
pods, err := client.ListPods(ctx, "") // Pods in the current namespace.
pods, err := client.ListPods(ctx, k8s.AllNamespaces) // Pods in all namespaces.
```

This can be overridden by explicitly passing a namespace.

```go
pods, err := client.ListPods(ctx, "custom-namespace") // Pods from the "custom-namespace"
```
Expand Down
15 changes: 8 additions & 7 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ import (
"github.com/golang/protobuf/proto"
)

const (
// AllNamespaces is given to list and watch operations to signify that the code should
// list or watch resources in all namespaces.
AllNamespaces = allNamespaces
// Actual definition is private in case we want to change it later.
allNamespaces = ""
)

// String returns a pointer to a string. Useful for creating API objects
// that take pointers instead of literals.
//
Expand Down Expand Up @@ -369,13 +377,6 @@ func (c *Client) client() *http.Client {
return c.Client
}

func (c *Client) namespaceFor(namespace string) string {
if namespace != "" {
return namespace
}
return c.Namespace
}

// The following methods hold the logic for interacting with the Kubernetes API. Generated
// clients are thin wrappers on top of these methods.
//
Expand Down
132 changes: 131 additions & 1 deletion client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
)

const skipMsg = `
warning: this package's test run using the default context of your "kubeclt" command,
warning: this package's test run using the default context of your "kubectl" command,
and will create resources on your cluster (mostly configmaps).

If you wish to continue set the following environment variable:
Expand Down Expand Up @@ -247,3 +247,133 @@ func TestWatch(t *testing.T) {
}
}
}

// TestWatchNamespace ensures that creating a configmap in a non-default namespace is not returned while watching the default namespace
func TestWatchNamespace(t *testing.T) {
client := newTestClient(t).CoreV1()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

defaultWatch, err := client.WatchConfigMaps(ctx, "default")
if err != nil {
t.Fatal(err)
}
defer defaultWatch.Close()

allWatch, err := client.WatchConfigMaps(ctx, AllNamespaces)
if err != nil {
t.Fatal(err)
}
defer allWatch.Close()

nonDefaultNamespaceName := newName()
defaultName := newName()
name := newName()
labelVal := newName()

// Create a configmap in the default namespace so the "default" watch has something to return
defaultCM := &v1.ConfigMap{
Metadata: &v1.ObjectMeta{
Name: String(defaultName),
Namespace: String("default"),
Labels: map[string]string{
"testLabel": labelVal,
},
},
Data: map[string]string{
"foo": "bar",
},
}
defaultGot, err := client.CreateConfigMap(ctx, defaultCM)
if err != nil {
t.Fatalf("create config map: %v", err)
}

// Create a non-default Namespace
ns := &v1.Namespace{
Metadata: &v1.ObjectMeta{
Name: String(nonDefaultNamespaceName),
},
}
if _, err := client.CreateNamespace(ctx, ns); err != nil {
t.Fatalf("create non-default-namespace: %v", err)
}

// Create a configmap in the non-default namespace
nonDefaultCM := &v1.ConfigMap{
Metadata: &v1.ObjectMeta{
Name: String(name),
Namespace: String(nonDefaultNamespaceName),
Labels: map[string]string{
"testLabel": labelVal,
},
},
Data: map[string]string{
"foo": "bar",
},
}
nonDefaultGot, err := client.CreateConfigMap(ctx, nonDefaultCM)
if err != nil {
t.Fatalf("create config map: %v", err)
}

// Watching the default namespace should not return the non-default namespace configmap,
// and instead return the previously created configmap in the default namespace
if _, gotFromWatch, err := defaultWatch.Next(); err != nil {
t.Errorf("failed to get next watch: %v", err)
} else {
if reflect.DeepEqual(nonDefaultGot, gotFromWatch) {
t.Errorf("config map in non-default namespace returned while watching default namespace")
}
if !reflect.DeepEqual(defaultGot, gotFromWatch) {
t.Errorf("object from add event did not match expected value")
}
}

// However, watching all-namespaces should contain both the default and non-default namespaced configmaps
if _, gotFromWatch, err := allWatch.Next(); err != nil {
t.Errorf("failed to get next watch: %v", err)
} else {
if !reflect.DeepEqual(defaultGot, gotFromWatch) {
t.Errorf("watching all namespaces did not return the expected configmap")
}
}

if _, gotFromWatch, err := allWatch.Next(); err != nil {
t.Errorf("failed to get next watch: %v", err)
} else {
if !reflect.DeepEqual(nonDefaultGot, gotFromWatch) {
t.Errorf("watching all namespaces did not return the expected configmap")
}
}

// Delete the config map in the default namespace first, then delete the non-default namespace config map.
// Only the former should be noticed by the default-watch.

if err := client.DeleteConfigMap(ctx, *defaultCM.Metadata.Name, *defaultCM.Metadata.Namespace); err != nil {
t.Fatalf("delete config map: %v", err)
}
if err := client.DeleteConfigMap(ctx, *nonDefaultCM.Metadata.Name, *nonDefaultCM.Metadata.Namespace); err != nil {
t.Fatalf("delete config map: %v", err)
}

if event, gotFromWatch, err := defaultWatch.Next(); err != nil {
t.Errorf("failed to get next watch: %v", err)
} else {
if *event.Type != EventDeleted {
t.Errorf("expected event type %q got %q", EventDeleted, *event.Type)
}

// Resource version will be different after a delete
nonDefaultGot.Metadata.ResourceVersion = String("")
gotFromWatch.Metadata.ResourceVersion = String("")

if reflect.DeepEqual(nonDefaultGot, gotFromWatch) {
t.Errorf("should not have received event from non-default namespace while watching default namespace")
}
}

if err := client.DeleteNamespace(ctx, nonDefaultNamespaceName); err != nil {
t.Fatalf("delete namespace: %v", err)
}
}
32 changes: 6 additions & 26 deletions gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (c *{{ $.Name }}) Create{{ $r.Name }}(ctx context.Context, obj *{{ $.Import
}

if {{ $r.Namespaced }} {
if ns = c.client.namespaceFor(ns); ns == "" {
if ns == "" {
return nil, fmt.Errorf("no resource namespace provided")
}
md.Namespace = &ns
Expand Down Expand Up @@ -144,7 +144,7 @@ func (c *{{ $.Name }}) Update{{ $r.Name }}(ctx context.Context, obj *{{ $.Import
}

if {{ $r.Namespaced }} {
if ns = c.client.namespaceFor(ns); ns == "" {
if ns == "" {
return nil, fmt.Errorf("no resource namespace provided")
}
md.Namespace = &ns
Expand All @@ -162,25 +162,15 @@ func (c *{{ $.Name }}) Delete{{ $r.Name }}(ctx context.Context, name string{{ if
if name == "" {
return fmt.Errorf("create: no name for given object")
}
{{ if $r.Namespaced -}}
ns := c.client.namespaceFor(namespace)
{{ else -}}
ns := ""
{{ end }}
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", ns, "{{ $r.Pluralized }}", name)
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", {{ if $r.Namespaced }}namespace{{ else }}AllNamespaces{{ end }}, "{{ $r.Pluralized }}", name)
return c.client.delete(ctx, pbCodec, url)
}

func (c *{{ $.Name }}) Get{{ $r.Name }}(ctx context.Context, name{{ if $r.Namespaced }}, namespace{{ end }} string) (*{{ $.ImportName }}.{{ $r.Name }}, error) {
if name == "" {
return nil, fmt.Errorf("create: no name for given object")
}
{{ if $r.Namespaced -}}
ns := c.client.namespaceFor(namespace)
{{ else -}}
ns := ""
{{ end }}
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", ns, "{{ $r.Pluralized }}", name)
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", {{ if $r.Namespaced }}namespace{{ else }}AllNamespaces{{ end }}, "{{ $r.Pluralized }}", name)
resp := new({{ $.ImportName }}.{{ $r.Name }})
if err := c.client.get(ctx, pbCodec, url, resp); err != nil {
return nil, err
Expand Down Expand Up @@ -211,12 +201,7 @@ func (w *{{ $.Name }}{{ $r.Name }}Watcher) Close() error {
}

func (c *{{ $.Name }}) Watch{{ $r.Name | pluralize }}(ctx context.Context{{ if $r.Namespaced }}, namespace string{{ end }}, options ...Option) (*{{ $.Name }}{{ $r.Name }}Watcher, error) {
{{ if $r.Namespaced -}}
ns := c.client.namespaceFor(namespace)
{{ else -}}
ns := ""
{{- end }}
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", ns, "{{ $r.Pluralized }}", "", options...)
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", {{ if $r.Namespaced }}namespace{{ else }}AllNamespaces{{ end }}, "{{ $r.Pluralized }}", "", options...)
watcher, err := c.client.watch(ctx, url)
if err != nil {
return nil, err
Expand All @@ -225,12 +210,7 @@ func (c *{{ $.Name }}) Watch{{ $r.Name | pluralize }}(ctx context.Context{{ if $
}

func (c *{{ $.Name }}) List{{ $r.Name | pluralize }}(ctx context.Context{{ if $r.Namespaced }}, namespace string{{ end }}, options ...Option) (*{{ $.ImportName }}.{{ $r.Name }}List, error) {
{{ if $r.Namespaced -}}
ns := c.client.namespaceFor(namespace)
{{ else -}}
ns := ""
{{- end }}
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", ns, "{{ $r.Pluralized }}", "", options...)
url := c.client.urlFor("{{ $.APIGroup }}", "{{ $.APIVersion }}", {{ if $r.Namespaced }}namespace{{ else }}AllNamespaces{{ end }}, "{{ $r.Pluralized }}", "", options...)
resp := new({{ $.ImportName }}.{{ $r.Name }}List)
if err := c.client.get(ctx, pbCodec, url, resp); err != nil {
return nil, err
Expand Down
25 changes: 10 additions & 15 deletions tprs.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,46 +115,41 @@ type object interface {
}

func (t *ThirdPartyResources) Create(ctx context.Context, resource, namespace string, req, resp interface{}) error {
ns := t.c.namespaceFor(namespace)
if err := checkResource(t.apiGroup, t.apiVersion, resource, ns, "not required"); err != nil {
if err := checkResource(t.apiGroup, t.apiVersion, resource, namespace, "not required"); err != nil {
return err
}
url := t.c.urlFor(t.apiGroup, t.apiVersion, ns, resource, "")
url := t.c.urlFor(t.apiGroup, t.apiVersion, namespace, resource, "")
return t.c.create(ctx, jsonCodec, "POST", url, req, resp)
}

func (t *ThirdPartyResources) Update(ctx context.Context, resource, namespace, name string, req, resp interface{}) error {
ns := t.c.namespaceFor(namespace)
if err := checkResource(t.apiGroup, t.apiVersion, resource, ns, "not required"); err != nil {
if err := checkResource(t.apiGroup, t.apiVersion, resource, namespace, "not required"); err != nil {
return err
}
url := t.c.urlFor(t.apiGroup, t.apiVersion, ns, resource, name)
url := t.c.urlFor(t.apiGroup, t.apiVersion, namespace, resource, name)
return t.c.create(ctx, jsonCodec, "PUT", url, req, resp)
}

func (t *ThirdPartyResources) Get(ctx context.Context, resource, namespace, name string, resp interface{}) error {
ns := t.c.namespaceFor(namespace)
if err := checkResource(t.apiGroup, t.apiVersion, resource, ns, name); err != nil {
if err := checkResource(t.apiGroup, t.apiVersion, resource, namespace, name); err != nil {
return err
}
url := t.c.urlFor(t.apiGroup, t.apiVersion, ns, resource, name)
url := t.c.urlFor(t.apiGroup, t.apiVersion, namespace, resource, name)
return t.c.get(ctx, jsonCodec, url, resp)
}

func (t *ThirdPartyResources) Delete(ctx context.Context, resource, namespace, name string) error {
ns := t.c.namespaceFor(namespace)
if err := checkResource(t.apiGroup, t.apiVersion, resource, ns, name); err != nil {
if err := checkResource(t.apiGroup, t.apiVersion, resource, namespace, name); err != nil {
return err
}
url := t.c.urlFor(t.apiGroup, t.apiVersion, ns, resource, name)
url := t.c.urlFor(t.apiGroup, t.apiVersion, namespace, resource, name)
return t.c.delete(ctx, jsonCodec, url)
}

func (t *ThirdPartyResources) List(ctx context.Context, resource, namespace string, resp interface{}) error {
ns := t.c.namespaceFor(namespace)
if err := checkResource(t.apiGroup, t.apiVersion, resource, ns, "name not required"); err != nil {
if err := checkResource(t.apiGroup, t.apiVersion, resource, namespace, "name not required"); err != nil {
return err
}
url := t.c.urlFor(t.apiGroup, t.apiVersion, ns, resource, "")
url := t.c.urlFor(t.apiGroup, t.apiVersion, namespace, resource, "")
return t.c.get(ctx, jsonCodec, url, resp)
}
Loading