Merge pull request #63557 from apelisse/dry-run-path

Automatic merge from submit-queue (batch tested with PRs 63603, 63557, 62015). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

apiserver: Fail if dry-run query param is specified

Adds a dry-run query parameter now that does nothing but reject the request. The sooner we have this check in master, the safer it will be for clients to send dry-run requests that are not going to be applied nonetheless.

```release-note
Create a new `dryRun` query parameter for mutating endpoints. If the parameter is set, then the query will be rejected, as the feature is not implemented yet. This will allow forward compatibility with future clients; otherwise, future clients talking with older apiservers might end up modifying a resource even if they include the `dryRun` query parameter.
```
This commit is contained in:
Kubernetes Submit Queue 2018-05-15 02:07:41 -07:00 committed by GitHub
commit 6aa6051fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 0 deletions

View File

@ -875,6 +875,71 @@ func TestUnimplementedRESTStorage(t *testing.T) {
}
}
type OnlyGetRESTStorage struct {
UnimplementedRESTStorage
}
func (OnlyGetRESTStorage) Get(ctx context.Context, id string, options *metav1.GetOptions) (runtime.Object, error) {
return nil, nil
}
func (OnlyGetRESTStorage) NewList() runtime.Object {
return &genericapitesting.SimpleList{}
}
func (OnlyGetRESTStorage) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
return nil, nil
}
// TestSomeUnimplementedRESTStorage ensures that if a rest.Storage does
// not implement a given method, that it is literally not registered
// with the server. We need to have at least one verb supported inorder
// to get a MethodNotAllowed rather than NotFound error.
func TestSomeUnimplementedRESTStorage(t *testing.T) {
type T struct {
Method string
Path string
ErrCode int
}
cases := map[string]T{
"groupless POST list": {"POST", "/" + grouplessPrefix + "/" + grouplessGroupVersion.Version + "/namespaces/default/foo", http.StatusMethodNotAllowed},
"groupless PUT object": {"PUT", "/" + grouplessPrefix + "/" + grouplessGroupVersion.Version + "/namespaces/default/foo/bar", http.StatusMethodNotAllowed},
"groupless DELETE object": {"DELETE", "/" + grouplessPrefix + "/" + grouplessGroupVersion.Version + "/namespaces/default/foo/bar", http.StatusMethodNotAllowed},
"groupless DELETE collection": {"DELETE", "/" + grouplessPrefix + "/" + grouplessGroupVersion.Version + "/namespaces/default/foo", http.StatusMethodNotAllowed},
"POST list": {"POST", "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/foo", http.StatusMethodNotAllowed},
"PUT object": {"PUT", "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/foo/bar", http.StatusMethodNotAllowed},
"DELETE object": {"DELETE", "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/foo/bar", http.StatusMethodNotAllowed},
"DELETE collection": {"DELETE", "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/foo", http.StatusMethodNotAllowed},
}
handler := handle(map[string]rest.Storage{
"foo": OnlyGetRESTStorage{},
})
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
for k, v := range cases {
request, err := http.NewRequest(v.Method, server.URL+v.Path, bytes.NewReader([]byte(`{"kind":"Simple","apiVersion":"version"}`)))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
response, err := client.Do(request)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer response.Body.Close()
data, err := ioutil.ReadAll(response.Body)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if response.StatusCode != v.ErrCode {
t.Errorf("%s: expected %d for %s, Got %s", k, v.ErrCode, v.Method, string(data))
continue
}
}
}
func TestList(t *testing.T) {
testCases := []struct {
url string
@ -3767,6 +3832,92 @@ func TestUpdateChecksAPIVersion(t *testing.T) {
}
}
// runRequest is used by TestDryRun since it runs the test twice in a
// row with a slightly different URL (one has ?dryRun, one doesn't).
func runRequest(t *testing.T, path, verb string, data []byte, contentType string) *http.Response {
request, err := http.NewRequest(verb, path, bytes.NewBuffer(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return response
}
// encodeOrFatal is used by TestDryRun to parse an object and stop right
// away if it fails.
func encodeOrFatal(t *testing.T, obj runtime.Object) []byte {
data, err := runtime.Encode(testCodec, obj)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return data
}
type SimpleRESTStorageWithDeleteCollection struct {
SimpleRESTStorage
}
// Delete collection doesn't do much, but let us test this path.
func (storage *SimpleRESTStorageWithDeleteCollection) DeleteCollection(ctx context.Context, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
storage.checkContext(ctx)
return nil, nil
}
func TestDryRun(t *testing.T) {
tests := []struct {
path string
verb string
data []byte
contentType string
}{
{path: "/namespaces/default/simples", verb: "POST", data: encodeOrFatal(t, &genericapitesting.Simple{Other: "bar"})},
{path: "/namespaces/default/simples/id", verb: "PUT", data: encodeOrFatal(t, &genericapitesting.Simple{ObjectMeta: metav1.ObjectMeta{Name: "id"}, Other: "bar"})},
{path: "/namespaces/default/simples/id", verb: "PATCH", data: []byte(`{"labels":{"foo":"bar"}}`), contentType: "application/merge-patch+json; charset=UTF-8"},
{path: "/namespaces/default/simples/id", verb: "DELETE"},
{path: "/namespaces/default/simples", verb: "DELETE"},
{path: "/namespaces/default/simples/id/subsimple", verb: "DELETE"},
}
server := httptest.NewServer(handle(map[string]rest.Storage{
"simples": &SimpleRESTStorageWithDeleteCollection{
SimpleRESTStorage{
item: genericapitesting.Simple{
ObjectMeta: metav1.ObjectMeta{
Name: "id",
Namespace: "",
UID: "uid",
},
Other: "bar",
},
},
},
"simples/subsimple": &SimpleXGSubresourceRESTStorage{
item: genericapitesting.SimpleXGSubresource{
SubresourceInfo: "foo",
},
itemGVK: testGroup2Version.WithKind("SimpleXGSubresource"),
},
}))
defer server.Close()
for _, test := range tests {
baseUrl := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version
response := runRequest(t, baseUrl+test.path, test.verb, test.data, test.contentType)
if response.StatusCode == http.StatusBadRequest {
t.Fatalf("unexpected BadRequest: %#v", response)
}
response = runRequest(t, baseUrl+test.path+"?dryRun", test.verb, test.data, test.contentType)
if response.StatusCode != http.StatusBadRequest {
t.Fatalf("unexpected non BadRequest: %#v", response)
}
}
}
type SimpleXGSubresourceRESTStorage struct {
item genericapitesting.SimpleXGSubresource
itemGVK schema.GroupVersionKind
@ -3782,6 +3933,10 @@ func (storage *SimpleXGSubresourceRESTStorage) Get(ctx context.Context, id strin
return storage.item.DeepCopyObject(), nil
}
func (storage *SimpleXGSubresourceRESTStorage) Delete(ctx context.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return nil, true, nil
}
func (storage *SimpleXGSubresourceRESTStorage) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
return storage.itemGVK
}

View File

@ -41,6 +41,11 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
trace := utiltrace.New("Create " + req.URL.Path)
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) {
scope.err(errors.NewBadRequest("dryRun is not supported yet"), w, req)
return
}
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.URL.Query().Get("timeout"))

View File

@ -41,6 +41,11 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco
trace := utiltrace.New("Delete " + req.URL.Path)
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) {
scope.err(errors.NewBadRequest("dryRun is not supported yet"), w, req)
return
}
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.URL.Query().Get("timeout"))
@ -167,6 +172,11 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco
// DeleteCollection returns a function that will handle a collection deletion
func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestScope, admit admission.Interface) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if isDryRun(req.URL) {
scope.err(errors.NewBadRequest("dryRun is not supported yet"), w, req)
return
}
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.URL.Query().Get("timeout"))

View File

@ -48,6 +48,11 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
trace := utiltrace.New("Patch " + req.URL.Path)
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) {
scope.err(errors.NewBadRequest("dryRun is not supported yet"), w, req)
return
}
// Do this first, otherwise name extraction can fail for unrecognized content types
// TODO: handle this in negotiation
contentType := req.Header.Get("Content-Type")

View File

@ -22,6 +22,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/golang/glog"
@ -318,3 +319,7 @@ func parseTimeout(str string) time.Duration {
}
return 30 * time.Second
}
func isDryRun(url *url.URL) bool {
return len(url.Query()["dryRun"]) != 0
}

View File

@ -40,6 +40,11 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
trace := utiltrace.New("Update " + req.URL.Path)
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) {
scope.err(errors.NewBadRequest("dryRun is not supported yet"), w, req)
return
}
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.URL.Query().Get("timeout"))