diff --git a/pkg/client/request.go b/pkg/client/request.go index 0a4698c92ee..44e202d3544 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -89,16 +89,22 @@ type Request struct { // whether to poll. poller PollFunc - // accessible via method setters - path string - params map[string]string - selector labels.Selector - sync bool - timeout time.Duration - // If true, put ns/ in path; if false, add "?namespace=" as a query parameter namespaceInPath bool + // generic components accessible via method setters + path string + subpath string + params map[string]string + + // structural elements of the request that are part of the Kubernetes API conventions + namespace string + resource string + resourceName string + selector labels.Selector + sync bool + timeout time.Duration + // output err error body io.Reader @@ -116,12 +122,50 @@ func NewRequest(client HTTPClient, verb string, baseURL *url.URL, codec runtime. } } -// Path appends an item to the request path. You must call Path at least once. -func (r *Request) Path(item string) *Request { +// Prefix adds segments to the relative beginning to the request path. These +// items will be placed before the optional Namespace, Resource, or Name sections. +// Setting AbsPath will clear any previously set Prefix segments +func (r *Request) Prefix(segments ...string) *Request { if r.err != nil { return r } - r.path = path.Join(r.path, item) + r.path = path.Join(r.path, path.Join(segments...)) + return r +} + +// Suffix appends segments to the end of the path. These items will be placed after the prefix and optional +// Namespace, Resource, or Name sections. +func (r *Request) Suffix(segments ...string) *Request { + if r.err != nil { + return r + } + r.subpath = path.Join(r.subpath, path.Join(segments...)) + return r +} + +// Resource sets the resource to access (/[ns//]) +func (r *Request) Resource(resource string) *Request { + if r.err != nil { + return r + } + if len(r.resource) != 0 { + r.err = fmt.Errorf("resource already set to %q, cannot change to %q", r.resource, resource) + return r + } + r.resource = resource + return r +} + +// Name sets the name of a resource to access (/[ns//]) +func (r *Request) Name(resourceName string) *Request { + if r.err != nil { + return r + } + if len(r.resourceName) != 0 { + r.err = fmt.Errorf("resource name already set to %q, cannot change to %q", r.resourceName, resourceName) + return r + } + r.resourceName = resourceName return r } @@ -134,29 +178,31 @@ func (r *Request) Sync(sync bool) *Request { return r } -// Namespace applies the namespace scope to a request +// Namespace applies the namespace scope to a request (/[ns//]) func (r *Request) Namespace(namespace string) *Request { if r.err != nil { return r } - - if len(namespace) > 0 { - if r.namespaceInPath { - return r.Path("ns").Path(namespace) - } else { - return r.setParam("namespace", namespace) - } - + if len(r.namespace) != 0 { + r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace) + return r } + r.namespace = namespace return r } -// AbsPath overwrites an existing path with the path parameter. -func (r *Request) AbsPath(path string) *Request { +// AbsPath overwrites an existing path with the segments provided. Trailing slashes are preserved +// when a single segment is passed. +func (r *Request) AbsPath(segments ...string) *Request { if r.err != nil { return r } - r.path = path + if len(segments) == 1 { + // preserve any trailing slashes for legacy behavior + r.path = segments[0] + } else { + r.path = path.Join(segments...) + } return r } @@ -273,13 +319,30 @@ func (r *Request) Poller(poller PollFunc) *Request { } func (r *Request) finalURL() string { + p := r.path + if r.namespaceInPath { + p = path.Join(p, "ns", r.namespace) + } + if len(r.resource) != 0 { + p = path.Join(p, r.resource) + } + // Join trims trailing slashes, so preserve r.path's trailing slash for backwards compat if nothing was changed + if len(r.resourceName) != 0 || len(r.subpath) != 0 { + p = path.Join(p, r.resourceName, r.subpath) + } + finalURL := *r.baseURL - finalURL.Path = r.path + finalURL.Path = p + query := url.Values{} for key, value := range r.params { query.Add(key, value) } + if !r.namespaceInPath && len(r.namespace) > 0 { + query.Add("namespace", r.namespace) + } + // sync and timeout are handled specially here, to allow setting them // in any order. if r.sync { @@ -452,13 +515,12 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request) ([]b switch resp.StatusCode { case http.StatusConflict: if req.Method == "POST" { - // TODO: add Resource() and ResourceName() as Request methods so that we can set these - err = errors.NewAlreadyExists("", "") + err = errors.NewAlreadyExists(r.resource, r.resourceName) } else { - err = errors.NewConflict("", "", err) + err = errors.NewConflict(r.resource, r.resourceName, err) } case http.StatusNotFound: - err = errors.NewNotFound("", "") + err = errors.NewNotFound(r.resource, r.resourceName) case http.StatusBadRequest: err = errors.NewBadRequest(err.Error()) }