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

Commit 2fc32e3

Browse files
authored
Merge pull request #84 from ericchiang/subresources
*: add support for subresources
2 parents 617b6a4 + 4526f7b commit 2fc32e3

File tree

6 files changed

+140
-57
lines changed

6 files changed

+140
-57
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,16 @@ l := new(k8s.LabelSelector)
204204
l.Eq("tier", "production")
205205
l.In("app", "database", "frontend")
206206

207-
pods, err := client.CoreV1().ListPods(ctx, client.Namespace, l.Selector())
207+
var pods corev1.PodList
208+
err := client.List(ctx, "custom-namespace", &pods, l.Selector())
209+
```
210+
211+
### Subresources
212+
213+
Access subresources using the `Subresource` option.
214+
215+
```go
216+
err := client.Update(ctx, &pod, k8s.Subresource("status"))
208217
```
209218

210219
### Creating out-of-cluster clients

client.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import (
3737
"net"
3838
"net/http"
3939
"os"
40-
"strconv"
4140
"time"
4241

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

117-
// Option represents optional call parameters, such as label selectors.
118-
type Option interface {
119-
queryParam() (key, val string)
120-
}
121-
122-
type queryParam struct {
123-
paramName string
124-
paramValue string
125-
}
126-
127-
func (o queryParam) queryParam() (string, string) {
128-
return o.paramName, o.paramValue
129-
}
130-
131-
// QueryParam can be used to manually set a URL query parameter by name.
132-
func QueryParam(name, value string) Option {
133-
return queryParam{
134-
paramName: name,
135-
paramValue: value,
136-
}
137-
}
138-
139-
type resourceVersionOption string
140-
141-
func (r resourceVersionOption) queryParam() (string, string) {
142-
return "resourceVersion", string(r)
143-
}
144-
145-
// ResourceVersion causes watch operations to only show changes since
146-
// a particular version of a resource.
147-
func ResourceVersion(resourceVersion string) Option {
148-
return resourceVersionOption(resourceVersion)
149-
}
150-
151-
type timeoutSeconds string
152-
153-
func (t timeoutSeconds) queryParam() (string, string) {
154-
return "timeoutSeconds", string(t)
155-
}
156-
157-
// Timeout declares the timeout for list and watch operations. Timeout
158-
// is only accurate to the second.
159-
func Timeout(d time.Duration) Option {
160-
return timeoutSeconds(strconv.FormatInt(int64(d/time.Second), 10))
161-
}
162-
163116
// NewClient initializes a client from a client config.
164117
func NewClient(config *Config) (*Client, error) {
165118
if len(config.Contexts) == 0 {

client_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ func TestListNodes(t *testing.T) {
9292
if err := client.List(context.TODO(), "", &nodes); err != nil {
9393
t.Fatal(err)
9494
}
95+
if len(nodes.Items) == 0 {
96+
t.Skip("no nodes in cluster")
97+
}
98+
9599
for _, node := range nodes.Items {
96100
if node.Metadata.Annotations == nil {
97101
node.Metadata.Annotations = map[string]string{}
@@ -111,6 +115,24 @@ func TestWithNamespace(t *testing.T) {
111115
withNamespace(t, func(client *k8s.Client, namespace string) {})
112116
}
113117

118+
func TestUpdateNamespaceStatus(t *testing.T) {
119+
withNamespace(t, func(client *k8s.Client, namespace string) {
120+
var ns corev1.Namespace
121+
if err := client.Get(context.TODO(), "", namespace, &ns); err != nil {
122+
t.Errorf("get namespace: %v", err)
123+
return
124+
}
125+
126+
if err := client.Update(context.TODO(), &ns, k8s.Subresource("status")); err != nil {
127+
t.Errorf("update namespace status subresource: %v", err)
128+
}
129+
130+
if err := client.Update(context.TODO(), &ns, k8s.Subresource("idontexist")); err == nil {
131+
t.Errorf("updated invalid subresource")
132+
}
133+
})
134+
}
135+
114136
func TestCreateConfigMap(t *testing.T) {
115137
withNamespace(t, func(client *k8s.Client, namespace string) {
116138
cm := &corev1.ConfigMap{

labels.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,8 @@ type LabelSelector struct {
2828
stmts []string
2929
}
3030

31-
type labelSelectorOption string
32-
33-
func (l labelSelectorOption) queryParam() (string, string) {
34-
return "labelSelector", string(l)
35-
}
36-
3731
func (l *LabelSelector) Selector() Option {
38-
return labelSelectorOption(l.String())
32+
return queryParam{"labelSelector", l.String()}
3933
}
4034

4135
func (l *LabelSelector) String() string {

resource.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,70 @@ import (
66
"net/url"
77
"path"
88
"reflect"
9+
"strconv"
910
"strings"
11+
"time"
1012

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

16+
// Option represents optional call parameters, such as label selectors.
17+
type Option interface {
18+
updateURL(base string, v url.Values) string
19+
}
20+
21+
type queryParam struct {
22+
paramName string
23+
paramValue string
24+
}
25+
26+
func (o queryParam) updateURL(base string, v url.Values) string {
27+
v.Set(o.paramName, o.paramValue)
28+
return base
29+
}
30+
31+
// QueryParam can be used to manually set a URL query parameter by name.
32+
func QueryParam(name, value string) Option {
33+
return queryParam{
34+
paramName: name,
35+
paramValue: value,
36+
}
37+
}
38+
39+
// ResourceVersion causes watch operations to only show changes since
40+
// a particular version of a resource.
41+
func ResourceVersion(resourceVersion string) Option {
42+
return queryParam{"resourceVersion", resourceVersion}
43+
}
44+
45+
// Timeout declares the timeout for list and watch operations. Timeout
46+
// is only accurate to the second.
47+
func Timeout(d time.Duration) Option {
48+
return queryParam{
49+
"timeoutSeconds",
50+
strconv.FormatInt(int64(d/time.Second), 10),
51+
}
52+
}
53+
54+
// Subresource is a way to interact with a part of an API object without needing
55+
// permissions on the entire resource. For example, a node isn't able to modify
56+
// a pod object, but can update the "pods/status" subresource.
57+
//
58+
// Common subresources are "status" and "scale".
59+
//
60+
// See https://kubernetes.io/docs/reference/api-concepts/
61+
func Subresource(name string) Option {
62+
return subresource{name}
63+
}
64+
65+
type subresource struct {
66+
name string
67+
}
68+
69+
func (s subresource) updateURL(base string, v url.Values) string {
70+
return base + "/" + s.name
71+
}
72+
1473
type resourceType struct {
1574
apiGroup string
1675
apiVersion string
@@ -74,8 +133,10 @@ func urlFor(endpoint, apiGroup, apiVersion, namespace, resource, name string, op
74133

75134
v := url.Values{}
76135
for _, option := range options {
77-
key, val := option.queryParam()
78-
v.Set(key, val)
136+
e = option.updateURL(e, v)
137+
}
138+
if len(v) == 0 {
139+
return e
79140
}
80141
return e + "?" + v.Encode()
81142
}

resource_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package k8s
22

33
import (
44
"testing"
5+
"time"
56

67
metav1 "github.com/ericchiang/k8s/apis/meta/v1"
78
)
@@ -97,6 +98,49 @@ func TestResourceURL(t *testing.T) {
9798
withName: true,
9899
want: "https://example.com/apis/apps/v1beta2/namespaces/my-namespace/deployments/my-deployment",
99100
},
101+
{
102+
name: "deployment-with-subresource",
103+
endpoint: "https://example.com",
104+
resource: &Deployment{
105+
Metadata: &metav1.ObjectMeta{
106+
Namespace: String("my-namespace"),
107+
Name: String("my-deployment"),
108+
},
109+
},
110+
withName: true,
111+
options: []Option{
112+
Subresource("status"),
113+
},
114+
want: "https://example.com/apis/apps/v1beta2/namespaces/my-namespace/deployments/my-deployment/status",
115+
},
116+
{
117+
name: "pod-with-timeout",
118+
endpoint: "https://example.com",
119+
resource: &Pod{
120+
Metadata: &metav1.ObjectMeta{
121+
Namespace: String("my-namespace"),
122+
Name: String("my-pod"),
123+
},
124+
},
125+
options: []Option{
126+
Timeout(time.Minute),
127+
},
128+
want: "https://example.com/api/v1/namespaces/my-namespace/pods?timeoutSeconds=60",
129+
},
130+
{
131+
name: "pod-with-resource-version",
132+
endpoint: "https://example.com",
133+
resource: &Pod{
134+
Metadata: &metav1.ObjectMeta{
135+
Namespace: String("my-namespace"),
136+
Name: String("my-pod"),
137+
},
138+
},
139+
options: []Option{
140+
ResourceVersion("foo"),
141+
},
142+
want: "https://example.com/api/v1/namespaces/my-namespace/pods?resourceVersion=foo",
143+
},
100144
}
101145

102146
for _, test := range tests {

0 commit comments

Comments
 (0)