Change:
A user report that it's broken in the slack channel:
Also and -- this probably broke with one of our recent refactors. The server should be accepting list options from any version, I thought?
FWIW, I think it's this line barf at the ListOptions: The parameter codec first decodes the query parameter to a versioned object and then convert it to the internal object. The code fails because it cannot create a "" ListOptions.
The server should accept list options in the API version it is sent. ListOptions should be registered in all third party groups.
The thirdPartyResourceDataCreator should handle "ListOptions", which surprises me that it doesn't then respect the group.
I think we should add a wrapper around the api.ParameterCodec here: Do something similar as what does.
Putting together a PR now and will let you guys review.
I guess my point is, the list options are just there from the URL query parameters. The user didn't send a group. Why do we assume that value for the group rather than "/v1" or something?
My understanding is we want to allow options to evolve differently in different groups. We do have v1.ListOptions and v1beta1.ListOptions. And if a client sends requeset to extensions/v1beta1 endpoint, then it's expected to encode the ListOptions to v1beta1 and then convert it to query parameters.
Mostly because the parameter codec is supposed to abstract that. "virtual" groups need to be implemented by the virtual "parameter codec". The statement on floating server objects (List, Delete etc) was that they have a locked API schema, and we have a mechanism to do that via the scheme which is registering them in a group. , Daniel Smith wrote:

Discussion:
OptionsExternalVersion: &optionsExternalVersion, Serializer:     thirdpartyresourcedata.NewNegotiatedSerializer(api.Codecs, kind, externalVersion, internalVersion), <del> ParameterCodec: api.ParameterCodec, </del> <ins> ParameterCodec: thirdpartyresourcedata.NewThirdPartyParameterCodec(api.ParameterCodec), </ins> Context: m.RequestContextMapper,

} if len(list.Items) != len(test.items) { <del> t.Errorf("unexpected length: %d vs %d", len(list.Items), len(test.items)) return </del> <ins> t.Fatalf("unexpected length: %d vs %d", len(list.Items), len(test.items)) </ins> } // The order of elements in LIST is not guaranteed. mapping := make(map[string]int)

resp, err := http.Post(server.URL+"/apis/company.com/"+version+"/namespaces/default/foos", "application/json", bytes.NewBuffer(data)) if !assert.NoError(err) { <del> t.Errorf("unexpected error: %v", err) return </del> <ins> t.Fatalf("unexpected error: %v", err) </ins> } assert.Equal(http.StatusCreated, resp.StatusCode)

return client.Do(req) } <ins> func TestInstallThirdPartyAPIListOptions(t *testing.T) { for _, version := range versionsToTest { testInstallThirdPartyAPIListOptionsForVersion(t, version) } } func testInstallThirdPartyAPIListOptionsForVersion(t *testing.T, version string) { _, etcdserver, server, assert := initThirdParty(t, version) // TODO: Uncomment when fix #19254 // defer server.Close() defer etcdserver.Terminate(t) // send a GET request with query parameter resp, err := httpGetWithRV(server.URL + "/apis/company.com/" + version + "/namespaces/default/foos") if !assert.NoError(err) { t.Fatalf("unexpected error: %v", err) } assert.Equal(http.StatusOK, resp.StatusCode) } func httpGetWithRV(url string) (*http.Response, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } q := req.URL.Query() // resourceversion is part of a ListOptions q.Add("resourceversion", "0") req.URL.RawQuery = q.Encode() client := &http.Client{} return client.Do(req) } </ins> func TestInstallThirdPartyResourceRemove(t *testing.T) { for _, version := range versionsToTest { testInstallThirdPartyResourceRemove(t, version)

"encoding/json" "fmt" "io" <ins> "net/url" </ins> "strings" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" apiutil "k8s.io/kubernetes/pkg/api/util" <ins> "k8s.io/kubernetes/pkg/api/v1" </ins> "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/runtime"

return t.delegate.New(kind) } } <ins> func NewThirdPartyParameterCodec(p runtime.ParameterCodec) runtime.ParameterCodec { return &thirdPartyParameterCodec{p} } type thirdPartyParameterCodec struct { delegate runtime.ParameterCodec } func (t *thirdPartyParameterCodec) DecodeParameters(parameters url.Values, from unversioned.GroupVersion, into runtime.Object) error { return t.delegate.DecodeParameters(parameters, v1.SchemeGroupVersion, into) } func (t *thirdPartyParameterCodec) EncodeParameters(obj runtime.Object, to unversioned.GroupVersion) (url.Values, error) { return t.delegate.EncodeParameters(obj, v1.SchemeGroupVersion) } </ins>