Skip to content

Commit efc7f86

Browse files
committed
Add GetterWithOptions and allow stream flushing
In addition to Getter interface, API Installer now supports a GetterWithOptions interface that takes an additional options object when getting a resource. A flag is now returned from rest.ResourceStreamer that indicates whether the streamed response should be flushed when written back to the client. This is to support log streaming.
1 parent 6dba6aa commit efc7f86

File tree

5 files changed

+176
-16
lines changed

5 files changed

+176
-16
lines changed

pkg/api/rest/rest.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
2929
)
3030

31-
// RESTStorage is a generic interface for RESTful storage services.
31+
// Storage is a generic interface for RESTful storage services.
3232
// Resources which are exported to the RESTful API of apiserver need to implement this interface. It is expected
3333
// that objects may implement any of the below interfaces.
3434
type Storage interface {
@@ -54,6 +54,21 @@ type Getter interface {
5454
Get(ctx api.Context, name string) (runtime.Object, error)
5555
}
5656

57+
// GetterWithOptions is an object that retrieve a named RESTful resource and takes
58+
// additional options on the get request
59+
type GetterWithOptions interface {
60+
// Get finds a resource in the storage by name and returns it.
61+
// Although it can return an arbitrary error value, IsNotFound(err) is true for the
62+
// returned error value err when the specified resource is not found.
63+
// The options object passed to it is of the same type returned by the NewGetOptions
64+
// method.
65+
Get(ctx api.Context, name string, options runtime.Object) (runtime.Object, error)
66+
67+
// NewGetOptions returns an empty options object that will be used to pass
68+
// options to the Get method.
69+
NewGetOptions() runtime.Object
70+
}
71+
5772
// Deleter is an object that can delete a named RESTful resource.
5873
type Deleter interface {
5974
// Delete finds a resource in the storage and deletes it.
@@ -119,6 +134,7 @@ type CreaterUpdater interface {
119134
// CreaterUpdater must satisfy the Updater interface.
120135
var _ Updater = CreaterUpdater(nil)
121136

137+
// Patcher is a storage object that supports both get and update.
122138
type Patcher interface {
123139
Getter
124140
Updater
@@ -153,11 +169,12 @@ type Redirector interface {
153169
// ResourceStreamer is an interface implemented by objects that prefer to be streamed from the server
154170
// instead of decoded directly.
155171
type ResourceStreamer interface {
156-
// InputStream should return an io.Reader if the provided object supports streaming. The desired
172+
// InputStream should return an io.ReadCloser if the provided object supports streaming. The desired
157173
// api version and a accept header (may be empty) are passed to the call. If no error occurs,
158-
// the caller may return a content type string with the reader that indicates the type of the
159-
// stream.
160-
InputStream(apiVersion, acceptHeader string) (io.ReadCloser, string, error)
174+
// the caller may return a flag indicating whether the result should be flushed as writes occur
175+
// and a content type string that indicates the type of the stream.
176+
// If a null stream is returned, a StatusNoContent response wil be generated.
177+
InputStream(apiVersion, acceptHeader string) (stream io.ReadCloser, flush bool, mimeType string, err error)
161178
}
162179

163180
// StorageMetadata is an optional interface that callers can implement to provide additional

pkg/apiserver/api_installer.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
130130
creater, isCreater := storage.(rest.Creater)
131131
lister, isLister := storage.(rest.Lister)
132132
getter, isGetter := storage.(rest.Getter)
133+
getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)
133134
deleter, isDeleter := storage.(rest.Deleter)
134135
gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
135136
updater, isUpdater := storage.(rest.Updater)
@@ -170,6 +171,17 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
170171
gracefulDeleter = rest.GracefulDeleteAdapter{deleter}
171172
}
172173

174+
var getOptions runtime.Object
175+
var getOptionsKind string
176+
if isGetterWithOptions {
177+
getOptions = getterWithOptions.NewGetOptions()
178+
_, getOptionsKind, err = a.group.Typer.ObjectVersionAndKind(getOptions)
179+
if err != nil {
180+
return err
181+
}
182+
isGetter = true
183+
}
184+
173185
var ctxFn ContextFunc
174186
ctxFn = func(req *restful.Request) api.Context {
175187
if ctx, ok := context.Get(req.Request); ok {
@@ -316,12 +328,23 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
316328
m := monitorFilter(action.Verb, resource)
317329
switch action.Verb {
318330
case "GET": // Get a resource.
319-
route := ws.GET(action.Path).To(GetResource(getter, reqScope)).
331+
var handler restful.RouteFunction
332+
if isGetterWithOptions {
333+
handler = GetResourceWithOptions(getterWithOptions, reqScope, getOptionsKind)
334+
} else {
335+
handler = GetResource(getter, reqScope)
336+
}
337+
route := ws.GET(action.Path).To(handler).
320338
Filter(m).
321339
Doc("read the specified " + kind).
322340
Operation("read" + kind).
323341
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
324342
Writes(versionedObject)
343+
if isGetterWithOptions {
344+
if err := addObjectParams(ws, route, getOptions); err != nil {
345+
return err
346+
}
347+
}
325348
addParams(route, action.Params)
326349
ws.Route(route)
327350
case "LIST": // List all resources of a kind.

pkg/apiserver/apiserver.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
3737
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
3838
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
39+
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/flushwriter"
3940
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
4041

4142
"github.com/emicklei/go-restful"
@@ -204,18 +205,27 @@ func APIVersionHandler(versions ...string) restful.RouteFunction {
204205
// be "application/octet-stream". All other objects are sent to standard JSON serialization.
205206
func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) {
206207
if stream, ok := object.(rest.ResourceStreamer); ok {
207-
out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))
208+
out, flush, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))
208209
if err != nil {
209210
errorJSONFatal(err, codec, w)
210211
return
211212
}
213+
if out == nil {
214+
// No output provided - return StatusNoContent
215+
w.WriteHeader(http.StatusNoContent)
216+
return
217+
}
212218
defer out.Close()
213219
if len(contentType) == 0 {
214220
contentType = "application/octet-stream"
215221
}
216222
w.Header().Set("Content-Type", contentType)
217223
w.WriteHeader(statusCode)
218-
io.Copy(w, out)
224+
writer := w.(io.Writer)
225+
if flush {
226+
writer = flushwriter.Wrap(w)
227+
}
228+
io.Copy(writer, out)
219229
return
220230
}
221231
writeJSON(statusCode, codec, object, w)

pkg/apiserver/apiserver_test.go

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,17 @@ func init() {
114114
// api.Status is returned in errors
115115

116116
// "internal" version
117-
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{})
117+
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{}, &SimpleGetOptions{})
118118
// "version" version
119119
// TODO: Use versioned api objects?
120-
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.Status{})
120+
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.Status{}, &SimpleGetOptions{})
121121
// "version2" version
122122
// TODO: Use versioned api objects?
123-
api.Scheme.AddKnownTypes(testVersion2, &Simple{}, &SimpleList{}, &v1beta3.Status{})
123+
api.Scheme.AddKnownTypes(testVersion2, &Simple{}, &SimpleList{}, &v1beta3.Status{}, &SimpleGetOptions{})
124+
125+
// Register SimpleGetOptions with the server versions to convert query params to it
126+
api.Scheme.AddKnownTypes("v1beta1", &SimpleGetOptions{})
127+
api.Scheme.AddKnownTypes("v1beta3", &SimpleGetOptions{})
124128

125129
nsMapper := newMapper()
126130
legacyNsMapper := newMapper()
@@ -231,6 +235,14 @@ type Simple struct {
231235

232236
func (*Simple) IsAnAPIObject() {}
233237

238+
type SimpleGetOptions struct {
239+
api.TypeMeta `json:",inline"`
240+
Param1 string `json:"param1"`
241+
Param2 string `json:"param2"`
242+
}
243+
244+
func (*SimpleGetOptions) IsAnAPIObject() {}
245+
234246
type SimpleList struct {
235247
api.TypeMeta `json:",inline"`
236248
api.ListMeta `json:"metadata,inline"`
@@ -254,6 +266,21 @@ func TestSimpleSetupRight(t *testing.T) {
254266
}
255267
}
256268

269+
func TestSimpleOptionsSetupRight(t *testing.T) {
270+
s := &SimpleGetOptions{}
271+
wire, err := codec.Encode(s)
272+
if err != nil {
273+
t.Fatal(err)
274+
}
275+
s2, err := codec.Decode(wire)
276+
if err != nil {
277+
t.Fatal(err)
278+
}
279+
if !reflect.DeepEqual(s, s2) {
280+
t.Fatalf("encode/decode broken:\n%#v\n%#v\n", s, s2)
281+
}
282+
}
283+
257284
type SimpleRESTStorage struct {
258285
errors map[string]error
259286
list []Simple
@@ -314,10 +341,10 @@ func (s *SimpleStream) Close() error {
314341

315342
func (s *SimpleStream) IsAnAPIObject() {}
316343

317-
func (s *SimpleStream) InputStream(version, accept string) (io.ReadCloser, string, error) {
344+
func (s *SimpleStream) InputStream(version, accept string) (io.ReadCloser, bool, string, error) {
318345
s.version = version
319346
s.accept = accept
320-
return s, s.contentType, s.err
347+
return s, false, s.contentType, s.err
321348
}
322349

323350
func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) {
@@ -432,6 +459,23 @@ func (m *MetadataRESTStorage) ProducesMIMETypes(method string) []string {
432459
return m.types
433460
}
434461

462+
type GetWithOptionsRESTStorage struct {
463+
*SimpleRESTStorage
464+
optionsReceived runtime.Object
465+
}
466+
467+
func (r *GetWithOptionsRESTStorage) Get(ctx api.Context, name string, options runtime.Object) (runtime.Object, error) {
468+
if _, ok := options.(*SimpleGetOptions); !ok {
469+
return nil, fmt.Errorf("Unexpected options object: %#v", options)
470+
}
471+
r.optionsReceived = options
472+
return r.SimpleRESTStorage.Get(ctx, name)
473+
}
474+
475+
func (r *GetWithOptionsRESTStorage) NewGetOptions() runtime.Object {
476+
return &SimpleGetOptions{}
477+
}
478+
435479
func extractBody(response *http.Response, object runtime.Object) (string, error) {
436480
defer response.Body.Close()
437481
body, err := ioutil.ReadAll(response.Body)
@@ -878,6 +922,47 @@ func TestGetBinary(t *testing.T) {
878922
}
879923
}
880924

925+
func TestGetWithOptions(t *testing.T) {
926+
storage := map[string]rest.Storage{}
927+
simpleStorage := GetWithOptionsRESTStorage{
928+
SimpleRESTStorage: &SimpleRESTStorage{
929+
item: Simple{
930+
Other: "foo",
931+
},
932+
},
933+
}
934+
storage["simple"] = &simpleStorage
935+
handler := handle(storage)
936+
server := httptest.NewServer(handler)
937+
defer server.Close()
938+
939+
resp, err := http.Get(server.URL + "/api/version/simple/id?param1=test1&param2=test2")
940+
if err != nil {
941+
t.Fatalf("unexpected error: %v", err)
942+
}
943+
if resp.StatusCode != http.StatusOK {
944+
t.Fatalf("unexpected response: %#v", resp)
945+
}
946+
var itemOut Simple
947+
body, err := extractBody(resp, &itemOut)
948+
if err != nil {
949+
t.Errorf("unexpected error: %v", err)
950+
}
951+
952+
if itemOut.Name != simpleStorage.item.Name {
953+
t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, simpleStorage.item, string(body))
954+
}
955+
956+
opts, ok := simpleStorage.optionsReceived.(*SimpleGetOptions)
957+
if !ok {
958+
t.Errorf("Unexpected options object received: %#v", simpleStorage.optionsReceived)
959+
return
960+
}
961+
if opts.Param1 != "test1" || opts.Param2 != "test2" {
962+
t.Errorf("Did not receive expected options: %#v", opts)
963+
}
964+
}
965+
881966
func TestGetAlternateSelfLink(t *testing.T) {
882967
storage := map[string]rest.Storage{}
883968
simpleStorage := SimpleRESTStorage{

pkg/apiserver/resthandler.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,13 @@ type RequestScope struct {
7474
ServerAPIVersion string
7575
}
7676

77-
// GetResource returns a function that handles retrieving a single resource from a rest.Storage object.
78-
func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction {
77+
// getterFunc performs a get request with the given context and object name. The request
78+
// may be used to deserialize an options object to pass to the getter.
79+
type getterFunc func(ctx api.Context, name string, req *restful.Request) (runtime.Object, error)
80+
81+
// getResourceHandler is an HTTP handler function for get requests. It delegates to the
82+
// passed-in getterFunc to perform the actual get.
83+
func getResourceHandler(scope RequestScope, getter getterFunc) restful.RouteFunction {
7984
return func(req *restful.Request, res *restful.Response) {
8085
w := res.ResponseWriter
8186
namespace, name, err := scope.Namer.Name(req)
@@ -86,7 +91,7 @@ func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction {
8691
ctx := scope.ContextFunc(req)
8792
ctx = api.WithNamespace(ctx, namespace)
8893

89-
result, err := r.Get(ctx, name)
94+
result, err := getter(ctx, name, req)
9095
if err != nil {
9196
errorJSON(err, scope.Codec, w)
9297
return
@@ -99,6 +104,26 @@ func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction {
99104
}
100105
}
101106

107+
// GetResource returns a function that handles retrieving a single resource from a rest.Storage object.
108+
func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction {
109+
return getResourceHandler(scope,
110+
func(ctx api.Context, name string, req *restful.Request) (runtime.Object, error) {
111+
return r.Get(ctx, name)
112+
})
113+
}
114+
115+
// GetResourceWithOptions returns a function that handles retrieving a single resource from a rest.Storage object.
116+
func GetResourceWithOptions(r rest.GetterWithOptions, scope RequestScope, getOptionsKind string) restful.RouteFunction {
117+
return getResourceHandler(scope,
118+
func(ctx api.Context, name string, req *restful.Request) (runtime.Object, error) {
119+
opts, err := queryToObject(req.Request.URL.Query(), scope, getOptionsKind)
120+
if err != nil {
121+
return nil, err
122+
}
123+
return r.Get(ctx, name, opts)
124+
})
125+
}
126+
102127
// ListResource returns a function that handles retrieving a list of resources from a rest.Storage object.
103128
func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch bool) restful.RouteFunction {
104129
return func(req *restful.Request, res *restful.Response) {

0 commit comments

Comments
 (0)