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

Commit

Permalink
Merge pull request #84 from ericchiang/subresources
Browse files Browse the repository at this point in the history
*: add support for subresources
  • Loading branch information
ericchiang authored Mar 19, 2018
2 parents 617b6a4 + 4526f7b commit 2fc32e3
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 57 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,16 @@ l := new(k8s.LabelSelector)
l.Eq("tier", "production")
l.In("app", "database", "frontend")

pods, err := client.CoreV1().ListPods(ctx, client.Namespace, l.Selector())
var pods corev1.PodList
err := client.List(ctx, "custom-namespace", &pods, l.Selector())
```

### Subresources

Access subresources using the `Subresource` option.

```go
err := client.Update(ctx, &pod, k8s.Subresource("status"))
```

### Creating out-of-cluster clients
Expand Down
47 changes: 0 additions & 47 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import (
"net"
"net/http"
"os"
"strconv"
"time"

"golang.org/x/net/http2"
Expand Down Expand Up @@ -114,52 +113,6 @@ func (c *Client) newRequest(ctx context.Context, verb, url string, body io.Reade
return req.WithContext(ctx), nil
}

// Option represents optional call parameters, such as label selectors.
type Option interface {
queryParam() (key, val string)
}

type queryParam struct {
paramName string
paramValue string
}

func (o queryParam) queryParam() (string, string) {
return o.paramName, o.paramValue
}

// QueryParam can be used to manually set a URL query parameter by name.
func QueryParam(name, value string) Option {
return queryParam{
paramName: name,
paramValue: value,
}
}

type resourceVersionOption string

func (r resourceVersionOption) queryParam() (string, string) {
return "resourceVersion", string(r)
}

// ResourceVersion causes watch operations to only show changes since
// a particular version of a resource.
func ResourceVersion(resourceVersion string) Option {
return resourceVersionOption(resourceVersion)
}

type timeoutSeconds string

func (t timeoutSeconds) queryParam() (string, string) {
return "timeoutSeconds", string(t)
}

// Timeout declares the timeout for list and watch operations. Timeout
// is only accurate to the second.
func Timeout(d time.Duration) Option {
return timeoutSeconds(strconv.FormatInt(int64(d/time.Second), 10))
}

// NewClient initializes a client from a client config.
func NewClient(config *Config) (*Client, error) {
if len(config.Contexts) == 0 {
Expand Down
22 changes: 22 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ func TestListNodes(t *testing.T) {
if err := client.List(context.TODO(), "", &nodes); err != nil {
t.Fatal(err)
}
if len(nodes.Items) == 0 {
t.Skip("no nodes in cluster")
}

for _, node := range nodes.Items {
if node.Metadata.Annotations == nil {
node.Metadata.Annotations = map[string]string{}
Expand All @@ -111,6 +115,24 @@ func TestWithNamespace(t *testing.T) {
withNamespace(t, func(client *k8s.Client, namespace string) {})
}

func TestUpdateNamespaceStatus(t *testing.T) {
withNamespace(t, func(client *k8s.Client, namespace string) {
var ns corev1.Namespace
if err := client.Get(context.TODO(), "", namespace, &ns); err != nil {
t.Errorf("get namespace: %v", err)
return
}

if err := client.Update(context.TODO(), &ns, k8s.Subresource("status")); err != nil {
t.Errorf("update namespace status subresource: %v", err)
}

if err := client.Update(context.TODO(), &ns, k8s.Subresource("idontexist")); err == nil {
t.Errorf("updated invalid subresource")
}
})
}

func TestCreateConfigMap(t *testing.T) {
withNamespace(t, func(client *k8s.Client, namespace string) {
cm := &corev1.ConfigMap{
Expand Down
8 changes: 1 addition & 7 deletions labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,8 @@ type LabelSelector struct {
stmts []string
}

type labelSelectorOption string

func (l labelSelectorOption) queryParam() (string, string) {
return "labelSelector", string(l)
}

func (l *LabelSelector) Selector() Option {
return labelSelectorOption(l.String())
return queryParam{"labelSelector", l.String()}
}

func (l *LabelSelector) String() string {
Expand Down
65 changes: 63 additions & 2 deletions resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,70 @@ import (
"net/url"
"path"
"reflect"
"strconv"
"strings"
"time"

metav1 "github.com/ericchiang/k8s/apis/meta/v1"
)

// Option represents optional call parameters, such as label selectors.
type Option interface {
updateURL(base string, v url.Values) string
}

type queryParam struct {
paramName string
paramValue string
}

func (o queryParam) updateURL(base string, v url.Values) string {
v.Set(o.paramName, o.paramValue)
return base
}

// QueryParam can be used to manually set a URL query parameter by name.
func QueryParam(name, value string) Option {
return queryParam{
paramName: name,
paramValue: value,
}
}

// ResourceVersion causes watch operations to only show changes since
// a particular version of a resource.
func ResourceVersion(resourceVersion string) Option {
return queryParam{"resourceVersion", resourceVersion}
}

// Timeout declares the timeout for list and watch operations. Timeout
// is only accurate to the second.
func Timeout(d time.Duration) Option {
return queryParam{
"timeoutSeconds",
strconv.FormatInt(int64(d/time.Second), 10),
}
}

// Subresource is a way to interact with a part of an API object without needing
// permissions on the entire resource. For example, a node isn't able to modify
// a pod object, but can update the "pods/status" subresource.
//
// Common subresources are "status" and "scale".
//
// See https://kubernetes.io/docs/reference/api-concepts/
func Subresource(name string) Option {
return subresource{name}
}

type subresource struct {
name string
}

func (s subresource) updateURL(base string, v url.Values) string {
return base + "/" + s.name
}

type resourceType struct {
apiGroup string
apiVersion string
Expand Down Expand Up @@ -74,8 +133,10 @@ func urlFor(endpoint, apiGroup, apiVersion, namespace, resource, name string, op

v := url.Values{}
for _, option := range options {
key, val := option.queryParam()
v.Set(key, val)
e = option.updateURL(e, v)
}
if len(v) == 0 {
return e
}
return e + "?" + v.Encode()
}
Expand Down
44 changes: 44 additions & 0 deletions resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package k8s

import (
"testing"
"time"

metav1 "github.com/ericchiang/k8s/apis/meta/v1"
)
Expand Down Expand Up @@ -97,6 +98,49 @@ func TestResourceURL(t *testing.T) {
withName: true,
want: "https://example.com/apis/apps/v1beta2/namespaces/my-namespace/deployments/my-deployment",
},
{
name: "deployment-with-subresource",
endpoint: "https://example.com",
resource: &Deployment{
Metadata: &metav1.ObjectMeta{
Namespace: String("my-namespace"),
Name: String("my-deployment"),
},
},
withName: true,
options: []Option{
Subresource("status"),
},
want: "https://example.com/apis/apps/v1beta2/namespaces/my-namespace/deployments/my-deployment/status",
},
{
name: "pod-with-timeout",
endpoint: "https://example.com",
resource: &Pod{
Metadata: &metav1.ObjectMeta{
Namespace: String("my-namespace"),
Name: String("my-pod"),
},
},
options: []Option{
Timeout(time.Minute),
},
want: "https://example.com/api/v1/namespaces/my-namespace/pods?timeoutSeconds=60",
},
{
name: "pod-with-resource-version",
endpoint: "https://example.com",
resource: &Pod{
Metadata: &metav1.ObjectMeta{
Namespace: String("my-namespace"),
Name: String("my-pod"),
},
},
options: []Option{
ResourceVersion("foo"),
},
want: "https://example.com/api/v1/namespaces/my-namespace/pods?resourceVersion=foo",
},
}

for _, test := range tests {
Expand Down

0 comments on commit 2fc32e3

Please sign in to comment.