2014-06-22 19:05:34 +00:00
|
|
|
/*
|
2016-06-03 00:25:58 +00:00
|
|
|
Copyright 2014 The Kubernetes Authors.
|
2014-06-22 19:05:34 +00:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2016-02-12 18:58:43 +00:00
|
|
|
package restclient
|
2014-06-22 19:05:34 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2016-07-13 03:44:55 +00:00
|
|
|
"encoding/hex"
|
2014-07-17 23:09:29 +00:00
|
|
|
"fmt"
|
2015-04-28 03:50:56 +00:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"mime"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
2015-12-20 19:36:12 +00:00
|
|
|
"reflect"
|
2015-04-28 03:50:56 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2015-08-05 22:05:17 +00:00
|
|
|
"github.com/golang/glog"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api/errors"
|
2015-09-09 21:59:11 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
2015-11-30 21:38:18 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api/v1"
|
2015-11-03 06:55:32 +00:00
|
|
|
"k8s.io/kubernetes/pkg/api/validation"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/client/metrics"
|
|
|
|
"k8s.io/kubernetes/pkg/fields"
|
|
|
|
"k8s.io/kubernetes/pkg/labels"
|
|
|
|
"k8s.io/kubernetes/pkg/runtime"
|
2016-04-26 07:05:40 +00:00
|
|
|
"k8s.io/kubernetes/pkg/runtime/serializer/streaming"
|
2016-03-09 05:54:59 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util/flowcontrol"
|
2016-01-06 15:56:41 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util/net"
|
2015-09-09 17:45:01 +00:00
|
|
|
"k8s.io/kubernetes/pkg/util/sets"
|
2015-08-05 22:03:47 +00:00
|
|
|
"k8s.io/kubernetes/pkg/watch"
|
2016-04-26 07:05:40 +00:00
|
|
|
"k8s.io/kubernetes/pkg/watch/versioned"
|
2014-06-22 19:05:34 +00:00
|
|
|
)
|
|
|
|
|
2016-02-17 12:02:28 +00:00
|
|
|
var (
|
|
|
|
// specialParams lists parameters that are handled specially and which users of Request
|
|
|
|
// are therefore not allowed to set manually.
|
|
|
|
specialParams = sets.NewString("timeout")
|
|
|
|
|
|
|
|
// longThrottleLatency defines threshold for logging requests. All requests being
|
|
|
|
// throttle for more than longThrottleLatency will be logged.
|
|
|
|
longThrottleLatency = 50 * time.Millisecond
|
|
|
|
)
|
2014-08-08 20:50:04 +00:00
|
|
|
|
2015-11-27 18:08:17 +00:00
|
|
|
func init() {
|
|
|
|
metrics.Register()
|
|
|
|
}
|
|
|
|
|
2014-10-29 02:48:13 +00:00
|
|
|
// HTTPClient is an interface for testing a request object.
|
|
|
|
type HTTPClient interface {
|
|
|
|
Do(req *http.Request) (*http.Response, error)
|
|
|
|
}
|
|
|
|
|
2015-08-25 11:56:08 +00:00
|
|
|
// ResponseWrapper is an interface for getting a response.
|
|
|
|
// The response may be either accessed as a raw data (the whole output is put into memory) or as a stream.
|
|
|
|
type ResponseWrapper interface {
|
|
|
|
DoRaw() ([]byte, error)
|
|
|
|
Stream() (io.ReadCloser, error)
|
|
|
|
}
|
|
|
|
|
2014-11-21 00:01:42 +00:00
|
|
|
// RequestConstructionError is returned when there's an error assembling a request.
|
|
|
|
type RequestConstructionError struct {
|
|
|
|
Err error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error returns a textual description of 'r'.
|
|
|
|
func (r *RequestConstructionError) Error() string {
|
|
|
|
return fmt.Sprintf("request construction error: '%v'", r.Err)
|
|
|
|
}
|
|
|
|
|
2014-06-22 19:05:34 +00:00
|
|
|
// Request allows for building up a request to a server in a chained fashion.
|
2014-06-23 00:02:48 +00:00
|
|
|
// Any errors are stored until the end of your call, so you only have to
|
|
|
|
// check once.
|
2014-06-22 19:05:34 +00:00
|
|
|
type Request struct {
|
2014-10-29 02:48:13 +00:00
|
|
|
// required
|
2015-12-25 23:05:01 +00:00
|
|
|
client HTTPClient
|
|
|
|
verb string
|
|
|
|
|
2016-04-26 07:05:40 +00:00
|
|
|
baseURL *url.URL
|
|
|
|
content ContentConfig
|
|
|
|
serializers Serializers
|
2014-10-29 02:48:13 +00:00
|
|
|
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
// generic components accessible via method setters
|
2016-01-06 23:59:54 +00:00
|
|
|
pathPrefix string
|
|
|
|
subpath string
|
|
|
|
params url.Values
|
|
|
|
headers http.Header
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
|
|
|
|
// structural elements of the request that are part of the Kubernetes API conventions
|
|
|
|
namespace string
|
2014-12-12 21:33:18 +00:00
|
|
|
namespaceSet bool
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
resource string
|
|
|
|
resourceName string
|
2015-03-09 03:52:53 +00:00
|
|
|
subresource string
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
selector labels.Selector
|
|
|
|
timeout time.Duration
|
|
|
|
|
2014-10-29 02:48:13 +00:00
|
|
|
// output
|
|
|
|
err error
|
|
|
|
body io.Reader
|
2015-03-03 22:55:56 +00:00
|
|
|
|
|
|
|
// The constructed request and the response
|
|
|
|
req *http.Request
|
|
|
|
resp *http.Response
|
2015-11-19 19:23:11 +00:00
|
|
|
|
|
|
|
backoffMgr BackoffManager
|
2016-03-09 05:54:59 +00:00
|
|
|
throttle flowcontrol.RateLimiter
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
|
|
|
|
2015-01-06 17:36:08 +00:00
|
|
|
// NewRequest creates a new request helper object for accessing runtime.Objects on a server.
|
2016-04-26 07:05:40 +00:00
|
|
|
func NewRequest(client HTTPClient, verb string, baseURL *url.URL, versionedAPIPath string, content ContentConfig, serializers Serializers, backoff BackoffManager, throttle flowcontrol.RateLimiter) *Request {
|
2015-11-19 19:23:11 +00:00
|
|
|
if backoff == nil {
|
|
|
|
glog.V(2).Infof("Not implementing request backoff strategy.")
|
|
|
|
backoff = &NoBackoff{}
|
|
|
|
}
|
2016-01-06 23:59:54 +00:00
|
|
|
|
|
|
|
pathPrefix := "/"
|
|
|
|
if baseURL != nil {
|
|
|
|
pathPrefix = path.Join(pathPrefix, baseURL.Path)
|
|
|
|
}
|
2015-12-25 23:05:01 +00:00
|
|
|
r := &Request{
|
2016-04-26 07:05:40 +00:00
|
|
|
client: client,
|
|
|
|
verb: verb,
|
|
|
|
baseURL: baseURL,
|
|
|
|
pathPrefix: path.Join(pathPrefix, versionedAPIPath),
|
|
|
|
content: content,
|
|
|
|
serializers: serializers,
|
|
|
|
backoffMgr: backoff,
|
|
|
|
throttle: throttle,
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
2016-07-13 03:44:55 +00:00
|
|
|
switch {
|
|
|
|
case len(content.AcceptContentTypes) > 0:
|
|
|
|
r.SetHeader("Accept", content.AcceptContentTypes)
|
|
|
|
case len(content.ContentType) > 0:
|
2015-12-25 23:05:01 +00:00
|
|
|
r.SetHeader("Accept", content.ContentType+", */*")
|
|
|
|
}
|
|
|
|
return r
|
2014-06-22 19:05:34 +00:00
|
|
|
}
|
|
|
|
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
// 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
|
|
|
|
}
|
2016-01-06 23:59:54 +00:00
|
|
|
r.pathPrefix = path.Join(r.pathPrefix, path.Join(segments...))
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
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 {
|
2014-06-22 19:05:34 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
r.subpath = path.Join(r.subpath, path.Join(segments...))
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resource sets the resource to access (<resource>/[ns/<namespace>/]<name>)
|
|
|
|
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
|
|
|
|
}
|
2015-12-16 06:03:20 +00:00
|
|
|
if msgs := validation.IsValidPathSegmentName(resource); len(msgs) != 0 {
|
|
|
|
r.err = fmt.Errorf("invalid resource %q: %v", resource, msgs)
|
2015-11-03 06:55:32 +00:00
|
|
|
return r
|
|
|
|
}
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
r.resource = resource
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-03-09 03:52:53 +00:00
|
|
|
// SubResource sets a sub-resource path which can be multiple segments segment after the resource
|
|
|
|
// name but before the suffix.
|
|
|
|
func (r *Request) SubResource(subresources ...string) *Request {
|
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
subresource := path.Join(subresources...)
|
|
|
|
if len(r.subresource) != 0 {
|
|
|
|
r.err = fmt.Errorf("subresource already set to %q, cannot change to %q", r.resource, subresource)
|
|
|
|
return r
|
|
|
|
}
|
2015-11-03 06:55:32 +00:00
|
|
|
for _, s := range subresources {
|
2015-12-16 06:03:20 +00:00
|
|
|
if msgs := validation.IsValidPathSegmentName(s); len(msgs) != 0 {
|
|
|
|
r.err = fmt.Errorf("invalid subresource %q: %v", s, msgs)
|
2015-11-03 06:55:32 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
}
|
2015-03-09 03:52:53 +00:00
|
|
|
r.subresource = subresource
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
// Name sets the name of a resource to access (<resource>/[ns/<namespace>/]<name>)
|
|
|
|
func (r *Request) Name(resourceName string) *Request {
|
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2015-03-20 22:22:51 +00:00
|
|
|
if len(resourceName) == 0 {
|
|
|
|
r.err = fmt.Errorf("resource name may not be empty")
|
|
|
|
return r
|
|
|
|
}
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
if len(r.resourceName) != 0 {
|
|
|
|
r.err = fmt.Errorf("resource name already set to %q, cannot change to %q", r.resourceName, resourceName)
|
|
|
|
return r
|
|
|
|
}
|
2015-12-16 06:03:20 +00:00
|
|
|
if msgs := validation.IsValidPathSegmentName(resourceName); len(msgs) != 0 {
|
|
|
|
r.err = fmt.Errorf("invalid resource name %q: %v", resourceName, msgs)
|
2015-11-03 06:55:32 +00:00
|
|
|
return r
|
|
|
|
}
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
r.resourceName = resourceName
|
2014-06-22 19:05:34 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
// Namespace applies the namespace scope to a request (<resource>/[ns/<namespace>/]<name>)
|
2014-10-03 15:44:06 +00:00
|
|
|
func (r *Request) Namespace(namespace string) *Request {
|
2014-10-29 02:48:59 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2014-12-12 21:33:18 +00:00
|
|
|
if r.namespaceSet {
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace)
|
|
|
|
return r
|
2014-10-03 15:44:06 +00:00
|
|
|
}
|
2015-12-16 06:03:20 +00:00
|
|
|
if msgs := validation.IsValidPathSegmentName(namespace); len(msgs) != 0 {
|
|
|
|
r.err = fmt.Errorf("invalid namespace %q: %v", namespace, msgs)
|
2015-11-03 06:55:32 +00:00
|
|
|
return r
|
|
|
|
}
|
2014-12-12 21:33:18 +00:00
|
|
|
r.namespaceSet = true
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
r.namespace = namespace
|
2014-10-03 15:44:06 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-02-16 04:43:45 +00:00
|
|
|
// NamespaceIfScoped is a convenience function to set a namespace if scoped is true
|
|
|
|
func (r *Request) NamespaceIfScoped(namespace string, scoped bool) *Request {
|
|
|
|
if scoped {
|
|
|
|
return r.Namespace(namespace)
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
// 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 {
|
2014-06-24 21:57:09 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2016-01-06 23:59:54 +00:00
|
|
|
r.pathPrefix = path.Join(r.baseURL.Path, path.Join(segments...))
|
|
|
|
if len(segments) == 1 && (len(r.baseURL.Path) > 1 || len(segments[0]) > 1) && strings.HasSuffix(segments[0], "/") {
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
// preserve any trailing slashes for legacy behavior
|
2016-01-06 23:59:54 +00:00
|
|
|
r.pathPrefix += "/"
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
}
|
2014-06-24 21:57:09 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-02-16 21:29:40 +00:00
|
|
|
// RequestURI overwrites existing path and parameters with the value of the provided server relative
|
|
|
|
// URI. Some parameters (those in specialParameters) cannot be overwritten.
|
|
|
|
func (r *Request) RequestURI(uri string) *Request {
|
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
locator, err := url.Parse(uri)
|
|
|
|
if err != nil {
|
|
|
|
r.err = err
|
|
|
|
return r
|
|
|
|
}
|
2016-01-06 23:59:54 +00:00
|
|
|
r.pathPrefix = locator.Path
|
2015-02-16 21:29:40 +00:00
|
|
|
if len(locator.Query()) > 0 {
|
|
|
|
if r.params == nil {
|
|
|
|
r.params = make(url.Values)
|
|
|
|
}
|
|
|
|
for k, v := range locator.Query() {
|
|
|
|
r.params[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-04-07 19:05:38 +00:00
|
|
|
const (
|
|
|
|
// A constant that clients can use to refer in a field selector to the object name field.
|
|
|
|
// Will be automatically emitted as the correct name for the API version.
|
2016-02-12 18:58:43 +00:00
|
|
|
nodeUnschedulable = "spec.unschedulable"
|
|
|
|
objectNameField = "metadata.name"
|
|
|
|
podHost = "spec.nodeName"
|
|
|
|
podStatus = "status.phase"
|
|
|
|
secretType = "type"
|
|
|
|
|
|
|
|
eventReason = "reason"
|
|
|
|
eventSource = "source"
|
|
|
|
eventType = "type"
|
|
|
|
eventInvolvedKind = "involvedObject.kind"
|
|
|
|
eventInvolvedNamespace = "involvedObject.namespace"
|
|
|
|
eventInvolvedName = "involvedObject.name"
|
|
|
|
eventInvolvedUID = "involvedObject.uid"
|
|
|
|
eventInvolvedAPIVersion = "involvedObject.apiVersion"
|
|
|
|
eventInvolvedResourceVersion = "involvedObject.resourceVersion"
|
|
|
|
eventInvolvedFieldPath = "involvedObject.fieldPath"
|
2015-04-07 19:05:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type clientFieldNameToAPIVersionFieldName map[string]string
|
|
|
|
|
|
|
|
func (c clientFieldNameToAPIVersionFieldName) filterField(field, value string) (newField, newValue string, err error) {
|
|
|
|
newFieldName, ok := c[field]
|
|
|
|
if !ok {
|
|
|
|
return "", "", fmt.Errorf("%v - %v - no field mapping defined", field, value)
|
2014-08-05 22:23:33 +00:00
|
|
|
}
|
2015-04-07 19:05:38 +00:00
|
|
|
return newFieldName, value, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type resourceTypeToFieldMapping map[string]clientFieldNameToAPIVersionFieldName
|
|
|
|
|
|
|
|
func (r resourceTypeToFieldMapping) filterField(resourceType, field, value string) (newField, newValue string, err error) {
|
|
|
|
fMapping, ok := r[resourceType]
|
|
|
|
if !ok {
|
|
|
|
return "", "", fmt.Errorf("%v - %v - %v - no field mapping defined", resourceType, field, value)
|
|
|
|
}
|
|
|
|
return fMapping.filterField(field, value)
|
|
|
|
}
|
|
|
|
|
2015-11-30 21:38:18 +00:00
|
|
|
type versionToResourceToFieldMapping map[unversioned.GroupVersion]resourceTypeToFieldMapping
|
2015-04-07 19:05:38 +00:00
|
|
|
|
2015-12-25 23:05:01 +00:00
|
|
|
func (v versionToResourceToFieldMapping) filterField(groupVersion *unversioned.GroupVersion, resourceType, field, value string) (newField, newValue string, err error) {
|
|
|
|
rMapping, ok := v[*groupVersion]
|
2015-04-07 19:05:38 +00:00
|
|
|
if !ok {
|
2015-11-30 21:38:18 +00:00
|
|
|
glog.Warningf("Field selector: %v - %v - %v - %v: need to check if this is versioned correctly.", groupVersion, resourceType, field, value)
|
2015-04-07 19:05:38 +00:00
|
|
|
return field, value, nil
|
2015-02-25 16:19:10 +00:00
|
|
|
}
|
2015-04-07 19:05:38 +00:00
|
|
|
newField, newValue, err = rMapping.filterField(resourceType, field, value)
|
2014-08-08 20:50:04 +00:00
|
|
|
if err != nil {
|
2015-04-07 19:05:38 +00:00
|
|
|
// This is only a warning until we find and fix all of the client's usages.
|
2015-11-30 21:38:18 +00:00
|
|
|
glog.Warningf("Field selector: %v - %v - %v - %v: need to check if this is versioned correctly.", groupVersion, resourceType, field, value)
|
2015-04-07 19:05:38 +00:00
|
|
|
return field, value, nil
|
2014-08-05 22:23:33 +00:00
|
|
|
}
|
2015-04-07 19:05:38 +00:00
|
|
|
return newField, newValue, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var fieldMappings = versionToResourceToFieldMapping{
|
2015-11-30 21:38:18 +00:00
|
|
|
v1.SchemeGroupVersion: resourceTypeToFieldMapping{
|
2015-04-07 19:05:38 +00:00
|
|
|
"nodes": clientFieldNameToAPIVersionFieldName{
|
2016-02-12 18:58:43 +00:00
|
|
|
objectNameField: objectNameField,
|
|
|
|
nodeUnschedulable: nodeUnschedulable,
|
2015-04-07 19:05:38 +00:00
|
|
|
},
|
|
|
|
"pods": clientFieldNameToAPIVersionFieldName{
|
2016-02-12 18:58:43 +00:00
|
|
|
podHost: podHost,
|
|
|
|
podStatus: podStatus,
|
2015-04-07 19:05:38 +00:00
|
|
|
},
|
2015-04-28 03:50:56 +00:00
|
|
|
"secrets": clientFieldNameToAPIVersionFieldName{
|
2016-02-12 18:58:43 +00:00
|
|
|
secretType: secretType,
|
2015-04-28 03:50:56 +00:00
|
|
|
},
|
2015-04-27 22:53:28 +00:00
|
|
|
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
|
2016-02-12 18:58:43 +00:00
|
|
|
objectNameField: objectNameField,
|
2015-04-27 22:53:28 +00:00
|
|
|
},
|
2015-06-16 23:21:54 +00:00
|
|
|
"endpoints": clientFieldNameToAPIVersionFieldName{
|
2016-02-12 18:58:43 +00:00
|
|
|
objectNameField: objectNameField,
|
2015-06-16 23:21:54 +00:00
|
|
|
},
|
|
|
|
"events": clientFieldNameToAPIVersionFieldName{
|
2016-02-12 18:58:43 +00:00
|
|
|
objectNameField: objectNameField,
|
|
|
|
eventReason: eventReason,
|
|
|
|
eventSource: eventSource,
|
|
|
|
eventType: eventType,
|
|
|
|
eventInvolvedKind: eventInvolvedKind,
|
|
|
|
eventInvolvedNamespace: eventInvolvedNamespace,
|
|
|
|
eventInvolvedName: eventInvolvedName,
|
|
|
|
eventInvolvedUID: eventInvolvedUID,
|
|
|
|
eventInvolvedAPIVersion: eventInvolvedAPIVersion,
|
|
|
|
eventInvolvedResourceVersion: eventInvolvedResourceVersion,
|
|
|
|
eventInvolvedFieldPath: eventInvolvedFieldPath,
|
2015-06-16 23:21:54 +00:00
|
|
|
},
|
2015-04-07 19:05:38 +00:00
|
|
|
},
|
|
|
|
}
|
2014-08-05 22:23:33 +00:00
|
|
|
|
2015-03-15 21:51:41 +00:00
|
|
|
// FieldsSelectorParam adds the given selector as a query parameter with the name paramName.
|
2015-04-06 23:54:26 +00:00
|
|
|
func (r *Request) FieldsSelectorParam(s fields.Selector) *Request {
|
2015-03-15 21:51:41 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2015-05-22 14:21:13 +00:00
|
|
|
if s == nil {
|
|
|
|
return r
|
|
|
|
}
|
2015-03-15 21:51:41 +00:00
|
|
|
if s.Empty() {
|
|
|
|
return r
|
|
|
|
}
|
2015-04-07 19:05:38 +00:00
|
|
|
s2, err := s.Transform(func(field, value string) (newField, newValue string, err error) {
|
2015-12-25 23:05:01 +00:00
|
|
|
return fieldMappings.filterField(r.content.GroupVersion, r.resource, field, value)
|
2015-04-07 19:05:38 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
r.err = err
|
|
|
|
return r
|
|
|
|
}
|
2015-12-25 23:05:01 +00:00
|
|
|
return r.setParam(unversioned.FieldSelectorQueryParam(r.content.GroupVersion.String()), s2.String())
|
2015-03-15 21:51:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// LabelsSelectorParam adds the given selector as a query parameter
|
2015-04-06 23:54:26 +00:00
|
|
|
func (r *Request) LabelsSelectorParam(s labels.Selector) *Request {
|
2014-06-22 19:05:34 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2015-05-22 14:21:13 +00:00
|
|
|
if s == nil {
|
|
|
|
return r
|
|
|
|
}
|
2014-12-31 00:30:18 +00:00
|
|
|
if s.Empty() {
|
|
|
|
return r
|
|
|
|
}
|
2015-12-25 23:05:01 +00:00
|
|
|
return r.setParam(unversioned.LabelSelectorQueryParam(r.content.GroupVersion.String()), s.String())
|
2014-06-22 19:05:34 +00:00
|
|
|
}
|
|
|
|
|
2014-08-05 22:23:33 +00:00
|
|
|
// UintParam creates a query parameter with the given value.
|
|
|
|
func (r *Request) UintParam(paramName string, u uint64) *Request {
|
2014-06-23 00:02:48 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2014-08-08 20:50:04 +00:00
|
|
|
return r.setParam(paramName, strconv.FormatUint(u, 10))
|
|
|
|
}
|
|
|
|
|
2014-10-07 20:51:28 +00:00
|
|
|
// Param creates a query parameter with the given string value.
|
|
|
|
func (r *Request) Param(paramName, s string) *Request {
|
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
return r.setParam(paramName, s)
|
|
|
|
}
|
|
|
|
|
2015-09-27 00:00:39 +00:00
|
|
|
// VersionedParams will take the provided object, serialize it to a map[string][]string using the
|
2015-12-25 23:05:18 +00:00
|
|
|
// implicit RESTClient API version and the default parameter codec, and then add those as parameters
|
2015-09-27 00:00:39 +00:00
|
|
|
// to the request. Use this to provide versioned query parameters from client libraries.
|
2015-12-25 23:05:18 +00:00
|
|
|
func (r *Request) VersionedParams(obj runtime.Object, codec runtime.ParameterCodec) *Request {
|
2015-09-27 00:00:39 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2015-12-25 23:05:18 +00:00
|
|
|
params, err := codec.EncodeParameters(obj, *r.content.GroupVersion)
|
2015-09-27 00:00:39 +00:00
|
|
|
if err != nil {
|
|
|
|
r.err = err
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
for k, v := range params {
|
2015-11-26 13:48:13 +00:00
|
|
|
for _, value := range v {
|
|
|
|
// TODO: Move it to setParam method, once we get rid of
|
|
|
|
// FieldSelectorParam & LabelSelectorParam methods.
|
2015-12-25 23:05:01 +00:00
|
|
|
if k == unversioned.LabelSelectorQueryParam(r.content.GroupVersion.String()) && value == "" {
|
2015-11-26 13:48:13 +00:00
|
|
|
// Don't set an empty selector for backward compatibility.
|
|
|
|
// Since there is no way to get the difference between empty
|
|
|
|
// and unspecified string, we don't set it to avoid having
|
|
|
|
// labelSelector= param in every request.
|
|
|
|
continue
|
|
|
|
}
|
2015-12-25 23:05:01 +00:00
|
|
|
if k == unversioned.FieldSelectorQueryParam(r.content.GroupVersion.String()) {
|
2015-12-21 05:32:52 +00:00
|
|
|
if len(value) == 0 {
|
2015-11-26 13:48:13 +00:00
|
|
|
// Don't set an empty selector for backward compatibility.
|
|
|
|
// Since there is no way to get the difference between empty
|
|
|
|
// and unspecified string, we don't set it to avoid having
|
|
|
|
// fieldSelector= param in every request.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// TODO: Filtering should be handled somewhere else.
|
|
|
|
selector, err := fields.ParseSelector(value)
|
|
|
|
if err != nil {
|
|
|
|
r.err = fmt.Errorf("unparsable field selector: %v", err)
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
filteredSelector, err := selector.Transform(
|
|
|
|
func(field, value string) (newField, newValue string, err error) {
|
2015-12-25 23:05:01 +00:00
|
|
|
return fieldMappings.filterField(r.content.GroupVersion, r.resource, field, value)
|
2015-11-26 13:48:13 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
r.err = fmt.Errorf("untransformable field selector: %v", err)
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
value = filteredSelector.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
r.setParam(k, value)
|
2015-09-27 00:00:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2014-08-08 20:50:04 +00:00
|
|
|
func (r *Request) setParam(paramName, value string) *Request {
|
|
|
|
if specialParams.Has(paramName) {
|
|
|
|
r.err = fmt.Errorf("must set %v through the corresponding function, not directly.", paramName)
|
|
|
|
return r
|
|
|
|
}
|
2014-10-29 02:48:13 +00:00
|
|
|
if r.params == nil {
|
2015-02-16 21:29:40 +00:00
|
|
|
r.params = make(url.Values)
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
2015-01-08 20:41:38 +00:00
|
|
|
r.params[paramName] = append(r.params[paramName], value)
|
2014-06-23 00:02:48 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-03-14 00:43:14 +00:00
|
|
|
func (r *Request) SetHeader(key, value string) *Request {
|
|
|
|
if r.headers == nil {
|
|
|
|
r.headers = http.Header{}
|
|
|
|
}
|
|
|
|
r.headers.Set(key, value)
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2014-08-05 22:23:33 +00:00
|
|
|
// Timeout makes the request use the given duration as a timeout. Sets the "timeout"
|
2015-01-22 05:20:57 +00:00
|
|
|
// parameter.
|
2014-06-22 19:05:34 +00:00
|
|
|
func (r *Request) Timeout(d time.Duration) *Request {
|
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
r.timeout = d
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2014-07-08 07:15:41 +00:00
|
|
|
// Body makes the request use obj as the body. Optional.
|
2014-06-22 19:05:34 +00:00
|
|
|
// If obj is a string, try to read a file of that name.
|
|
|
|
// If obj is a []byte, send it directly.
|
2014-08-05 22:23:33 +00:00
|
|
|
// If obj is an io.Reader, use it directly.
|
2015-10-20 20:31:44 +00:00
|
|
|
// If obj is a runtime.Object, marshal it correctly, and set Content-Type header.
|
2015-12-20 19:36:12 +00:00
|
|
|
// If obj is a runtime.Object and nil, do nothing.
|
2014-09-06 02:22:03 +00:00
|
|
|
// Otherwise, set an error.
|
2014-06-22 19:05:34 +00:00
|
|
|
func (r *Request) Body(obj interface{}) *Request {
|
|
|
|
if r.err != nil {
|
|
|
|
return r
|
|
|
|
}
|
2014-06-23 00:02:48 +00:00
|
|
|
switch t := obj.(type) {
|
|
|
|
case string:
|
2014-08-08 20:50:04 +00:00
|
|
|
data, err := ioutil.ReadFile(t)
|
|
|
|
if err != nil {
|
2014-06-23 00:02:48 +00:00
|
|
|
r.err = err
|
2014-08-08 20:50:04 +00:00
|
|
|
return r
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
2016-07-13 18:37:16 +00:00
|
|
|
glog.V(8).Infof("Request Body: %#v", string(data))
|
2016-05-14 18:06:57 +00:00
|
|
|
r.body = bytes.NewReader(data)
|
2014-06-23 00:02:48 +00:00
|
|
|
case []byte:
|
2016-07-13 18:37:16 +00:00
|
|
|
glog.V(8).Infof("Request Body: %#v", string(t))
|
2016-05-14 18:06:57 +00:00
|
|
|
r.body = bytes.NewReader(t)
|
2014-06-24 21:57:09 +00:00
|
|
|
case io.Reader:
|
2014-08-05 22:23:33 +00:00
|
|
|
r.body = t
|
2014-09-06 02:22:03 +00:00
|
|
|
case runtime.Object:
|
2015-12-20 19:36:12 +00:00
|
|
|
// callers may pass typed interface pointers, therefore we must check nil with reflection
|
|
|
|
if reflect.ValueOf(t).IsNil() {
|
|
|
|
return r
|
|
|
|
}
|
2016-04-26 07:05:40 +00:00
|
|
|
data, err := runtime.Encode(r.serializers.Encoder, t)
|
2014-08-08 20:50:04 +00:00
|
|
|
if err != nil {
|
2014-06-23 00:02:48 +00:00
|
|
|
r.err = err
|
2014-08-08 20:50:04 +00:00
|
|
|
return r
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
2016-07-13 18:37:16 +00:00
|
|
|
glog.V(8).Infof("Request Body: %#v", string(data))
|
2016-05-14 18:06:57 +00:00
|
|
|
r.body = bytes.NewReader(data)
|
2015-12-25 23:05:01 +00:00
|
|
|
r.SetHeader("Content-Type", r.content.ContentType)
|
2014-09-06 02:22:03 +00:00
|
|
|
default:
|
2014-12-19 20:28:51 +00:00
|
|
|
r.err = fmt.Errorf("unknown type used for body: %+v", obj)
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
2014-06-22 19:05:34 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-04-30 03:27:13 +00:00
|
|
|
// URL returns the current working URL.
|
|
|
|
func (r *Request) URL() *url.URL {
|
2016-01-06 23:59:54 +00:00
|
|
|
p := r.pathPrefix
|
2015-06-15 22:15:55 +00:00
|
|
|
if r.namespaceSet && len(r.namespace) > 0 {
|
2015-01-19 21:50:00 +00:00
|
|
|
p = path.Join(p, "namespaces", r.namespace)
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
}
|
|
|
|
if len(r.resource) != 0 {
|
2015-06-15 22:15:55 +00:00
|
|
|
p = path.Join(p, strings.ToLower(r.resource))
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
}
|
2016-06-22 09:40:52 +00:00
|
|
|
// Join trims trailing slashes, so preserve r.pathPrefix's trailing slash for backwards compatibility if nothing was changed
|
2015-03-09 03:52:53 +00:00
|
|
|
if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 {
|
|
|
|
p = path.Join(p, r.resourceName, r.subresource, r.subpath)
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
}
|
|
|
|
|
2015-04-30 03:27:13 +00:00
|
|
|
finalURL := &url.URL{}
|
2015-04-03 07:06:07 +00:00
|
|
|
if r.baseURL != nil {
|
2015-04-30 03:27:13 +00:00
|
|
|
*finalURL = *r.baseURL
|
2015-04-03 07:06:07 +00:00
|
|
|
}
|
Introduce Resource/ResourceName/Prefix/Suffix options to RESTClient
RESTClient is an abstraction for simplifying access to resources that
follow the Kubernetes API pattern. Currently, both Namespace and Path
are coupled, which means changes across versions is complex. In general,
most access to resources should be to a resource collection (e.g.
"services") with a name (e.g. "foo"). Other constructs, like prefix sections
("watch") or proposed suffix sections ("/pods/foo/spec") only modify this
core pattern.
This commit removes the Path() helper from Request and introduces:
* Prefix(segments ...string) - segments that should go to the beginning of the path.
* Suffix(segments ...string) - segments that should go to the end of the path.
* Resource(string) - collection name, should be after prefix
* Namespace(string) - if specified, should be set after resource but before name
* Name(string) - if specified, should be after namespace
Now, only Prefix and Suffix are order dependent (and with variadics, should be
simpler). Resource, Namespace, and Name may be specified in any order.
Path() has been removed to prevent downstream consumers of RESTClient from experiencing
behavior change.
2014-12-23 21:14:32 +00:00
|
|
|
finalURL.Path = p
|
|
|
|
|
2014-07-17 23:09:29 +00:00
|
|
|
query := url.Values{}
|
2015-01-08 20:41:38 +00:00
|
|
|
for key, values := range r.params {
|
|
|
|
for _, value := range values {
|
|
|
|
query.Add(key, value)
|
|
|
|
}
|
2014-07-17 23:09:29 +00:00
|
|
|
}
|
2014-10-03 15:44:06 +00:00
|
|
|
|
2015-01-22 05:20:57 +00:00
|
|
|
// timeout is handled specially here.
|
|
|
|
if r.timeout != 0 {
|
2015-02-16 21:29:40 +00:00
|
|
|
query.Set("timeout", r.timeout.String())
|
2014-07-17 23:09:29 +00:00
|
|
|
}
|
2014-09-30 00:15:00 +00:00
|
|
|
finalURL.RawQuery = query.Encode()
|
2015-04-30 03:27:13 +00:00
|
|
|
return finalURL
|
2014-07-17 23:09:29 +00:00
|
|
|
}
|
|
|
|
|
2015-06-10 16:52:28 +00:00
|
|
|
// finalURLTemplate is similar to URL(), but will make all specific parameter values equal
|
|
|
|
// - instead of name or namespace, "{name}" and "{namespace}" will be used, and all query
|
|
|
|
// parameters will be reset. This creates a copy of the request so as not to change the
|
|
|
|
// underyling object. This means some useful request info (like the types of field
|
|
|
|
// selectors in use) will be lost.
|
|
|
|
// TODO: preserve field selector keys
|
|
|
|
func (r Request) finalURLTemplate() string {
|
2015-04-03 07:06:07 +00:00
|
|
|
if len(r.resourceName) != 0 {
|
2015-06-10 16:52:28 +00:00
|
|
|
r.resourceName = "{name}"
|
2015-04-03 07:06:07 +00:00
|
|
|
}
|
2015-06-10 16:52:28 +00:00
|
|
|
if r.namespaceSet && len(r.namespace) != 0 {
|
|
|
|
r.namespace = "{namespace}"
|
|
|
|
}
|
|
|
|
newParams := url.Values{}
|
|
|
|
v := []string{"{value}"}
|
|
|
|
for k := range r.params {
|
|
|
|
newParams[k] = v
|
|
|
|
}
|
|
|
|
r.params = newParams
|
2015-04-30 03:27:13 +00:00
|
|
|
return r.URL().String()
|
2015-04-03 07:06:07 +00:00
|
|
|
}
|
|
|
|
|
2016-02-17 12:02:28 +00:00
|
|
|
func (r *Request) tryThrottle() {
|
|
|
|
now := time.Now()
|
|
|
|
if r.throttle != nil {
|
|
|
|
r.throttle.Accept()
|
|
|
|
}
|
|
|
|
if latency := time.Since(now); latency > longThrottleLatency {
|
2016-03-18 04:19:53 +00:00
|
|
|
glog.V(4).Infof("Throttling request took %v, request: %s:%s", latency, r.verb, r.URL().String())
|
2016-02-17 12:02:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// Watch attempts to begin watching the requested location.
|
|
|
|
// Returns a watch.Interface, or an error.
|
2014-07-17 23:09:29 +00:00
|
|
|
func (r *Request) Watch() (watch.Interface, error) {
|
2016-02-11 21:50:05 +00:00
|
|
|
// We specifically don't want to rate limit watches, so we
|
|
|
|
// don't use r.throttle here.
|
2014-07-17 23:09:29 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return nil, r.err
|
|
|
|
}
|
2016-04-23 19:00:28 +00:00
|
|
|
if r.serializers.Framer == nil {
|
|
|
|
return nil, fmt.Errorf("watching resources is not possible with this client (content-type: %s)", r.content.ContentType)
|
|
|
|
}
|
|
|
|
|
2015-04-30 03:27:13 +00:00
|
|
|
url := r.URL().String()
|
|
|
|
req, err := http.NewRequest(r.verb, url, r.body)
|
2014-07-17 23:09:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-05-09 07:33:13 +00:00
|
|
|
req.Header = r.headers
|
2014-10-29 02:48:13 +00:00
|
|
|
client := r.client
|
2014-09-30 00:15:00 +00:00
|
|
|
if client == nil {
|
|
|
|
client = http.DefaultClient
|
2014-07-17 23:09:29 +00:00
|
|
|
}
|
2016-02-24 16:56:22 +00:00
|
|
|
r.backoffMgr.Sleep(r.backoffMgr.CalculateBackoff(r.URL()))
|
2014-10-29 02:48:13 +00:00
|
|
|
resp, err := client.Do(req)
|
2015-10-25 14:43:21 +00:00
|
|
|
updateURLMetrics(r, resp, err)
|
2015-11-19 19:23:11 +00:00
|
|
|
if r.baseURL != nil {
|
|
|
|
if err != nil {
|
|
|
|
r.backoffMgr.UpdateBackoff(r.baseURL, err, 0)
|
|
|
|
} else {
|
|
|
|
r.backoffMgr.UpdateBackoff(r.baseURL, err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
}
|
2014-07-17 23:09:29 +00:00
|
|
|
if err != nil {
|
2015-03-25 10:42:44 +00:00
|
|
|
// The watch stream mechanism handles many common partial data errors, so closed
|
|
|
|
// connections can be retried in many cases.
|
2016-01-06 15:56:41 +00:00
|
|
|
if net.IsProbableEOF(err) {
|
2014-12-18 20:38:24 +00:00
|
|
|
return watch.NewEmptyWatch(), nil
|
|
|
|
}
|
2014-07-17 23:09:29 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2014-10-29 02:48:13 +00:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
2016-02-04 03:07:00 +00:00
|
|
|
defer resp.Body.Close()
|
2015-04-10 05:10:35 +00:00
|
|
|
if result := r.transformResponse(resp, req); result.err != nil {
|
|
|
|
return nil, result.err
|
2014-11-12 21:31:24 +00:00
|
|
|
}
|
2015-04-30 03:27:13 +00:00
|
|
|
return nil, fmt.Errorf("for request '%+v', got status: %v", url, resp.StatusCode)
|
2014-07-17 23:09:29 +00:00
|
|
|
}
|
2016-04-26 07:05:40 +00:00
|
|
|
framer := r.serializers.Framer.NewFrameReader(resp.Body)
|
|
|
|
decoder := streaming.NewDecoder(framer, r.serializers.StreamingSerializer)
|
|
|
|
return watch.NewStreamWatcher(versioned.NewDecoder(decoder, r.serializers.Decoder)), nil
|
2014-07-17 23:09:29 +00:00
|
|
|
}
|
|
|
|
|
2015-10-25 14:43:21 +00:00
|
|
|
// updateURLMetrics is a convenience function for pushing metrics.
|
|
|
|
// It also handles corner cases for incomplete/invalid request data.
|
|
|
|
func updateURLMetrics(req *Request, resp *http.Response, err error) {
|
|
|
|
url := "none"
|
|
|
|
if req.baseURL != nil {
|
|
|
|
url = req.baseURL.Host
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we have an error (i.e. apiserver down) we report that as a metric label.
|
|
|
|
if err != nil {
|
|
|
|
metrics.RequestResult.WithLabelValues(err.Error(), req.verb, url).Inc()
|
|
|
|
} else {
|
|
|
|
//Metrics for failure codes
|
|
|
|
metrics.RequestResult.WithLabelValues(strconv.Itoa(resp.StatusCode), req.verb, url).Inc()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-02 00:19:00 +00:00
|
|
|
// Stream formats and executes the request, and offers streaming of the response.
|
|
|
|
// Returns io.ReadCloser which could be used for streaming of the response, or an error
|
2015-04-08 15:32:13 +00:00
|
|
|
// Any non-2xx http status code causes an error. If we get a non-2xx code, we try to convert the body into an APIStatus object.
|
|
|
|
// If we can, we return that as an error. Otherwise, we create an error that lists the http status and the content of the response.
|
2014-10-02 00:19:00 +00:00
|
|
|
func (r *Request) Stream() (io.ReadCloser, error) {
|
|
|
|
if r.err != nil {
|
|
|
|
return nil, r.err
|
|
|
|
}
|
2016-02-11 21:50:05 +00:00
|
|
|
|
2016-02-17 12:02:28 +00:00
|
|
|
r.tryThrottle()
|
2016-02-11 21:50:05 +00:00
|
|
|
|
2015-04-30 03:27:13 +00:00
|
|
|
url := r.URL().String()
|
|
|
|
req, err := http.NewRequest(r.verb, url, nil)
|
2014-10-02 00:19:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-05-09 07:33:13 +00:00
|
|
|
req.Header = r.headers
|
2014-10-29 02:48:13 +00:00
|
|
|
client := r.client
|
2014-10-02 00:19:00 +00:00
|
|
|
if client == nil {
|
|
|
|
client = http.DefaultClient
|
|
|
|
}
|
2016-02-24 16:56:22 +00:00
|
|
|
r.backoffMgr.Sleep(r.backoffMgr.CalculateBackoff(r.URL()))
|
2014-10-29 02:48:13 +00:00
|
|
|
resp, err := client.Do(req)
|
2015-10-25 14:43:21 +00:00
|
|
|
updateURLMetrics(r, resp, err)
|
2015-11-19 19:23:11 +00:00
|
|
|
if r.baseURL != nil {
|
|
|
|
if err != nil {
|
|
|
|
r.backoffMgr.UpdateBackoff(r.URL(), err, 0)
|
|
|
|
} else {
|
|
|
|
r.backoffMgr.UpdateBackoff(r.URL(), err, resp.StatusCode)
|
|
|
|
}
|
|
|
|
}
|
2014-10-02 00:19:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-08 15:32:13 +00:00
|
|
|
|
|
|
|
switch {
|
|
|
|
case (resp.StatusCode >= 200) && (resp.StatusCode < 300):
|
|
|
|
return resp.Body, nil
|
|
|
|
|
|
|
|
default:
|
2015-07-10 15:08:54 +00:00
|
|
|
// ensure we close the body before returning the error
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2015-04-08 15:32:13 +00:00
|
|
|
// we have a decent shot at taking the object returned, parsing it as a status object and returning a more normal error
|
|
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2015-04-30 03:27:13 +00:00
|
|
|
return nil, fmt.Errorf("%v while accessing %v", resp.Status, url)
|
2015-04-08 15:32:13 +00:00
|
|
|
}
|
|
|
|
|
2016-04-26 07:05:40 +00:00
|
|
|
// TODO: Check ContentType.
|
|
|
|
if runtimeObject, err := runtime.Decode(r.serializers.Decoder, bodyBytes); err == nil {
|
2015-04-08 15:32:13 +00:00
|
|
|
statusError := errors.FromObject(runtimeObject)
|
|
|
|
|
2015-11-30 22:43:52 +00:00
|
|
|
if _, ok := statusError.(errors.APIStatus); ok {
|
2015-04-08 15:32:13 +00:00
|
|
|
return nil, statusError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyText := string(bodyBytes)
|
2015-04-30 03:27:13 +00:00
|
|
|
return nil, fmt.Errorf("%s while accessing %v: %s", resp.Status, url, bodyText)
|
2015-04-08 15:32:13 +00:00
|
|
|
}
|
2014-10-02 00:19:00 +00:00
|
|
|
}
|
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
// request connects to the server and invokes the provided function when a server response is
|
2015-10-12 10:57:56 +00:00
|
|
|
// received. It handles retry behavior and up front validation of requests. It will invoke
|
2015-08-08 21:29:57 +00:00
|
|
|
// fn at most once. It will return an error if a problem occurred prior to connecting to the
|
2015-04-10 05:10:35 +00:00
|
|
|
// server - the provided function is responsible for handling server errors.
|
|
|
|
func (r *Request) request(fn func(*http.Request, *http.Response)) error {
|
2015-10-25 14:43:21 +00:00
|
|
|
//Metrics for total request latency
|
|
|
|
start := time.Now()
|
|
|
|
defer func() {
|
|
|
|
metrics.RequestLatency.WithLabelValues(r.verb, r.finalURLTemplate()).Observe(metrics.SinceInMicroseconds(start))
|
|
|
|
}()
|
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
if r.err != nil {
|
2015-11-19 19:23:11 +00:00
|
|
|
glog.V(4).Infof("Error in request: %v", r.err)
|
2015-04-10 05:10:35 +00:00
|
|
|
return r.err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: added to catch programmer errors (invoking operations with an object with an empty namespace)
|
|
|
|
if (r.verb == "GET" || r.verb == "PUT" || r.verb == "DELETE") && r.namespaceSet && len(r.resourceName) > 0 && len(r.namespace) == 0 {
|
|
|
|
return fmt.Errorf("an empty namespace may not be set when a resource name is provided")
|
|
|
|
}
|
|
|
|
if (r.verb == "POST") && r.namespaceSet && len(r.namespace) == 0 {
|
|
|
|
return fmt.Errorf("an empty namespace may not be set during creation")
|
|
|
|
}
|
|
|
|
|
2014-10-29 02:48:13 +00:00
|
|
|
client := r.client
|
|
|
|
if client == nil {
|
|
|
|
client = http.DefaultClient
|
|
|
|
}
|
|
|
|
|
2015-01-22 01:12:07 +00:00
|
|
|
// Right now we make about ten retry attempts if we get a Retry-After response.
|
|
|
|
// TODO: Change to a timeout based approach.
|
2015-04-10 05:10:35 +00:00
|
|
|
maxRetries := 10
|
2015-01-22 01:12:07 +00:00
|
|
|
retries := 0
|
2014-06-26 23:10:38 +00:00
|
|
|
for {
|
2015-04-30 03:27:13 +00:00
|
|
|
url := r.URL().String()
|
|
|
|
req, err := http.NewRequest(r.verb, url, r.body)
|
2014-06-26 23:10:38 +00:00
|
|
|
if err != nil {
|
2015-04-10 05:10:35 +00:00
|
|
|
return err
|
2014-06-26 23:10:38 +00:00
|
|
|
}
|
2015-04-10 05:10:35 +00:00
|
|
|
req.Header = r.headers
|
|
|
|
|
2016-02-24 16:56:22 +00:00
|
|
|
r.backoffMgr.Sleep(r.backoffMgr.CalculateBackoff(r.URL()))
|
2015-04-10 05:10:35 +00:00
|
|
|
resp, err := client.Do(req)
|
2015-10-25 14:43:21 +00:00
|
|
|
updateURLMetrics(r, resp, err)
|
2015-11-19 19:23:11 +00:00
|
|
|
if err != nil {
|
|
|
|
r.backoffMgr.UpdateBackoff(r.URL(), err, 0)
|
|
|
|
} else {
|
|
|
|
r.backoffMgr.UpdateBackoff(r.URL(), err, resp.StatusCode)
|
|
|
|
}
|
2014-06-26 23:10:38 +00:00
|
|
|
if err != nil {
|
2015-04-10 05:10:35 +00:00
|
|
|
return err
|
2014-06-23 01:14:32 +00:00
|
|
|
}
|
2015-04-10 05:10:35 +00:00
|
|
|
|
|
|
|
done := func() bool {
|
|
|
|
// ensure the response body is closed before we reconnect, so that we reuse the same
|
|
|
|
// TCP connection
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
retries++
|
|
|
|
if seconds, wait := checkWait(resp); wait && retries < maxRetries {
|
2016-05-14 18:06:57 +00:00
|
|
|
if seeker, ok := r.body.(io.Seeker); ok && r.body != nil {
|
|
|
|
_, err := seeker.Seek(0, 0)
|
|
|
|
if err != nil {
|
|
|
|
glog.V(4).Infof("Could not retry request, can't Seek() back to beginning of body for %T", r.body)
|
|
|
|
fn(req, resp)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
glog.V(4).Infof("Got a Retry-After %s response for attempt %d to %v", seconds, retries, url)
|
2016-02-24 16:56:22 +00:00
|
|
|
r.backoffMgr.Sleep(time.Duration(seconds) * time.Second)
|
2015-04-10 05:10:35 +00:00
|
|
|
return false
|
2015-01-22 01:12:07 +00:00
|
|
|
}
|
2015-04-10 05:10:35 +00:00
|
|
|
fn(req, resp)
|
|
|
|
return true
|
|
|
|
}()
|
|
|
|
if done {
|
|
|
|
return nil
|
2015-01-22 01:12:07 +00:00
|
|
|
}
|
2014-06-23 01:14:32 +00:00
|
|
|
}
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
|
|
|
|
2015-03-03 22:55:56 +00:00
|
|
|
// Do formats and executes the request. Returns a Result object for easy response
|
|
|
|
// processing.
|
|
|
|
//
|
|
|
|
// Error type:
|
|
|
|
// * If the request can't be constructed, or an error happened earlier while building its
|
|
|
|
// arguments: *RequestConstructionError
|
|
|
|
// * If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError
|
|
|
|
// * http.Client.Do errors are returned directly.
|
|
|
|
func (r *Request) Do() Result {
|
2016-02-17 12:02:28 +00:00
|
|
|
r.tryThrottle()
|
2016-02-11 21:50:05 +00:00
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
var result Result
|
|
|
|
err := r.request(func(req *http.Request, resp *http.Response) {
|
|
|
|
result = r.transformResponse(resp, req)
|
|
|
|
})
|
2014-10-29 02:48:13 +00:00
|
|
|
if err != nil {
|
2015-03-03 22:55:56 +00:00
|
|
|
return Result{err: err}
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
2015-04-10 05:10:35 +00:00
|
|
|
return result
|
2015-03-03 22:55:56 +00:00
|
|
|
}
|
2014-10-29 02:48:13 +00:00
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
// DoRaw executes the request but does not process the response body.
|
|
|
|
func (r *Request) DoRaw() ([]byte, error) {
|
2016-02-17 12:02:28 +00:00
|
|
|
r.tryThrottle()
|
2016-02-11 21:50:05 +00:00
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
var result Result
|
|
|
|
err := r.request(func(req *http.Request, resp *http.Response) {
|
|
|
|
result.body, result.err = ioutil.ReadAll(resp.Body)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return result.body, result.err
|
|
|
|
}
|
|
|
|
|
|
|
|
// transformResponse converts an API response into a structured API object
|
|
|
|
func (r *Request) transformResponse(resp *http.Response, req *http.Request) Result {
|
|
|
|
var body []byte
|
|
|
|
if resp.Body != nil {
|
2015-03-24 03:15:35 +00:00
|
|
|
if data, err := ioutil.ReadAll(resp.Body); err == nil {
|
|
|
|
body = data
|
|
|
|
}
|
|
|
|
}
|
2016-07-13 03:44:55 +00:00
|
|
|
|
|
|
|
if glog.V(8) {
|
|
|
|
switch {
|
|
|
|
case bytes.IndexFunc(body, func(r rune) bool { return r < 0x0a }) != -1:
|
|
|
|
glog.Infof("Response Body:\n%s", hex.Dump(body))
|
|
|
|
default:
|
|
|
|
glog.Infof("Response Body: %s", string(body))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// verify the content type is accurate
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
|
|
decoder := r.serializers.Decoder
|
|
|
|
if len(contentType) > 0 && (decoder == nil || (len(r.content.ContentType) > 0 && contentType != r.content.ContentType)) {
|
|
|
|
mediaType, params, err := mime.ParseMediaType(contentType)
|
|
|
|
if err != nil {
|
|
|
|
return Result{err: errors.NewInternalError(err)}
|
|
|
|
}
|
|
|
|
decoder, err = r.serializers.RenegotiatedDecoder(mediaType, params)
|
|
|
|
if err != nil {
|
|
|
|
// if we fail to negotiate a decoder, treat this as an unstructured error
|
|
|
|
switch {
|
|
|
|
case resp.StatusCode == http.StatusSwitchingProtocols:
|
|
|
|
// no-op, we've been upgraded
|
|
|
|
case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
|
|
|
|
return Result{err: r.transformUnstructuredResponseError(resp, req, body)}
|
|
|
|
}
|
|
|
|
return Result{
|
|
|
|
body: body,
|
|
|
|
contentType: contentType,
|
|
|
|
statusCode: resp.StatusCode,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-06-18 17:00:29 +00:00
|
|
|
|
2014-10-29 02:48:13 +00:00
|
|
|
// Did the server give us a status response?
|
|
|
|
isStatusResponse := false
|
2016-07-13 03:44:55 +00:00
|
|
|
status := &unversioned.Status{}
|
2016-02-04 21:51:37 +00:00
|
|
|
// Because release-1.1 server returns Status with empty APIVersion at paths
|
|
|
|
// to the Extensions resources, we need to use DecodeInto here to provide
|
|
|
|
// default groupVersion, otherwise a status response won't be correctly
|
|
|
|
// decoded.
|
2016-07-13 03:44:55 +00:00
|
|
|
err := runtime.DecodeInto(decoder, body, status)
|
2016-02-04 21:51:37 +00:00
|
|
|
if err == nil && len(status.Status) > 0 {
|
2014-10-29 02:48:13 +00:00
|
|
|
isStatusResponse = true
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
2015-01-08 20:41:38 +00:00
|
|
|
case resp.StatusCode == http.StatusSwitchingProtocols:
|
|
|
|
// no-op, we've been upgraded
|
2014-10-29 02:48:13 +00:00
|
|
|
case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
|
|
|
|
if !isStatusResponse {
|
2015-04-10 05:10:35 +00:00
|
|
|
return Result{err: r.transformUnstructuredResponseError(resp, req, body)}
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
2015-12-21 05:32:52 +00:00
|
|
|
return Result{err: errors.FromObject(status)}
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the server gave us a status back, look at what it was.
|
2015-03-03 22:55:56 +00:00
|
|
|
success := resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent
|
2015-09-09 21:59:11 +00:00
|
|
|
if isStatusResponse && (status.Status != unversioned.StatusSuccess && !success) {
|
2014-10-29 02:48:13 +00:00
|
|
|
// "Failed" requests are clearly just an error and it makes sense to return them as such.
|
2015-12-21 05:32:52 +00:00
|
|
|
return Result{err: errors.FromObject(status)}
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
return Result{
|
2015-12-25 23:05:01 +00:00
|
|
|
body: body,
|
2016-05-10 15:09:14 +00:00
|
|
|
contentType: contentType,
|
2015-12-25 23:05:01 +00:00
|
|
|
statusCode: resp.StatusCode,
|
2016-05-10 15:09:14 +00:00
|
|
|
decoder: decoder,
|
2015-04-10 05:10:35 +00:00
|
|
|
}
|
2014-10-29 02:48:13 +00:00
|
|
|
}
|
|
|
|
|
2015-03-24 03:15:35 +00:00
|
|
|
// transformUnstructuredResponseError handles an error from the server that is not in a structured form.
|
2015-03-26 21:24:17 +00:00
|
|
|
// It is expected to transform any response that is not recognizable as a clear server sent error from the
|
|
|
|
// K8S API using the information provided with the request. In practice, HTTP proxies and client libraries
|
|
|
|
// introduce a level of uncertainty to the responses returned by servers that in common use result in
|
|
|
|
// unexpected responses. The rough structure is:
|
|
|
|
//
|
|
|
|
// 1. Assume the server sends you something sane - JSON + well defined error objects + proper codes
|
|
|
|
// - this is the happy path
|
|
|
|
// - when you get this output, trust what the server sends
|
|
|
|
// 2. Guard against empty fields / bodies in received JSON and attempt to cull sufficient info from them to
|
|
|
|
// generate a reasonable facsimile of the original failure.
|
|
|
|
// - Be sure to use a distinct error type or flag that allows a client to distinguish between this and error 1 above
|
|
|
|
// 3. Handle true disconnect failures / completely malformed data by moving up to a more generic client error
|
|
|
|
// 4. Distinguish between various connection failures like SSL certificates, timeouts, proxy errors, unexpected
|
|
|
|
// initial contact, the presence of mismatched body contents from posted content types
|
|
|
|
// - Give these a separate distinct error type and capture as much as possible of the original message
|
|
|
|
//
|
|
|
|
// TODO: introduce transformation of generic http.Client.Do() errors that separates 4.
|
2015-03-24 03:15:35 +00:00
|
|
|
func (r *Request) transformUnstructuredResponseError(resp *http.Response, req *http.Request, body []byte) error {
|
|
|
|
if body == nil && resp.Body != nil {
|
|
|
|
if data, err := ioutil.ReadAll(resp.Body); err == nil {
|
|
|
|
body = data
|
|
|
|
}
|
|
|
|
}
|
2016-07-13 18:37:16 +00:00
|
|
|
glog.V(8).Infof("Response Body: %#v", string(body))
|
2015-06-18 17:00:29 +00:00
|
|
|
|
2015-03-24 03:15:35 +00:00
|
|
|
message := "unknown"
|
|
|
|
if isTextResponse(resp) {
|
|
|
|
message = strings.TrimSpace(string(body))
|
|
|
|
}
|
2015-04-02 03:20:09 +00:00
|
|
|
retryAfter, _ := retryAfterSeconds(resp)
|
2015-12-25 23:05:01 +00:00
|
|
|
return errors.NewGenericServerResponse(
|
|
|
|
resp.StatusCode,
|
|
|
|
req.Method,
|
|
|
|
unversioned.GroupResource{
|
|
|
|
Group: r.content.GroupVersion.Group,
|
|
|
|
Resource: r.resource,
|
|
|
|
},
|
|
|
|
r.resourceName,
|
|
|
|
message,
|
|
|
|
retryAfter,
|
|
|
|
true,
|
|
|
|
)
|
2015-03-24 03:15:35 +00:00
|
|
|
}
|
|
|
|
|
2015-03-24 02:56:22 +00:00
|
|
|
// isTextResponse returns true if the response appears to be a textual media type.
|
|
|
|
func isTextResponse(resp *http.Response) bool {
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
|
|
if len(contentType) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
media, _, err := mime.ParseMediaType(contentType)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return strings.HasPrefix(media, "text/")
|
|
|
|
}
|
|
|
|
|
2015-04-10 05:10:35 +00:00
|
|
|
// checkWait returns true along with a number of seconds if the server instructed us to wait
|
|
|
|
// before retrying.
|
|
|
|
func checkWait(resp *http.Response) (int, bool) {
|
2015-11-04 20:15:01 +00:00
|
|
|
switch r := resp.StatusCode; {
|
|
|
|
// any 500 error code and 429 can trigger a wait
|
|
|
|
case r == errors.StatusTooManyRequests, r >= 500:
|
|
|
|
default:
|
2015-04-10 05:10:35 +00:00
|
|
|
return 0, false
|
|
|
|
}
|
|
|
|
i, ok := retryAfterSeconds(resp)
|
|
|
|
return i, ok
|
|
|
|
}
|
|
|
|
|
2015-03-26 21:24:17 +00:00
|
|
|
// retryAfterSeconds returns the value of the Retry-After header and true, or 0 and false if
|
2015-03-24 02:56:22 +00:00
|
|
|
// the header was missing or not a valid number.
|
2015-03-26 21:24:17 +00:00
|
|
|
func retryAfterSeconds(resp *http.Response) (int, bool) {
|
2015-03-24 02:56:22 +00:00
|
|
|
if h := resp.Header.Get("Retry-After"); len(h) > 0 {
|
|
|
|
if i, err := strconv.Atoi(h); err == nil {
|
|
|
|
return i, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0, false
|
|
|
|
}
|
|
|
|
|
2014-06-23 00:02:48 +00:00
|
|
|
// Result contains the result of calling Request.Do().
|
|
|
|
type Result struct {
|
2015-12-25 23:05:01 +00:00
|
|
|
body []byte
|
|
|
|
contentType string
|
|
|
|
err error
|
|
|
|
statusCode int
|
2014-10-24 17:16:02 +00:00
|
|
|
|
2015-12-25 23:05:01 +00:00
|
|
|
decoder runtime.Decoder
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Raw returns the raw result.
|
|
|
|
func (r Result) Raw() ([]byte, error) {
|
|
|
|
return r.body, r.err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get returns the result as an object.
|
2014-09-06 02:22:03 +00:00
|
|
|
func (r Result) Get() (runtime.Object, error) {
|
2014-06-23 00:02:48 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return nil, r.err
|
|
|
|
}
|
2016-05-10 15:09:14 +00:00
|
|
|
if r.decoder == nil {
|
|
|
|
return nil, fmt.Errorf("serializer for %s doesn't exist", r.contentType)
|
|
|
|
}
|
2015-12-25 23:05:01 +00:00
|
|
|
return runtime.Decode(r.decoder, r.body)
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
|
|
|
|
2015-06-19 00:53:21 +00:00
|
|
|
// StatusCode returns the HTTP status code of the request. (Only valid if no
|
|
|
|
// error was returned.)
|
|
|
|
func (r Result) StatusCode(statusCode *int) Result {
|
|
|
|
*statusCode = r.statusCode
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-12-21 05:32:52 +00:00
|
|
|
// Into stores the result into obj, if possible. If obj is nil it is ignored.
|
2014-09-06 02:22:03 +00:00
|
|
|
func (r Result) Into(obj runtime.Object) error {
|
2014-06-23 00:02:48 +00:00
|
|
|
if r.err != nil {
|
|
|
|
return r.err
|
2014-06-22 19:05:34 +00:00
|
|
|
}
|
2016-05-10 15:09:14 +00:00
|
|
|
if r.decoder == nil {
|
|
|
|
return fmt.Errorf("serializer for %s doesn't exist", r.contentType)
|
|
|
|
}
|
2015-12-25 23:05:01 +00:00
|
|
|
return runtime.DecodeInto(r.decoder, r.body, obj)
|
2014-06-23 00:02:48 +00:00
|
|
|
}
|
|
|
|
|
2014-10-24 17:16:02 +00:00
|
|
|
// WasCreated updates the provided bool pointer to whether the server returned
|
|
|
|
// 201 created or a different response.
|
|
|
|
func (r Result) WasCreated(wasCreated *bool) Result {
|
2015-06-19 00:53:21 +00:00
|
|
|
*wasCreated = r.statusCode == http.StatusCreated
|
2014-10-24 17:16:02 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// Error returns the error executing the request, nil if no error occurred.
|
2014-11-21 00:01:42 +00:00
|
|
|
// See the Request.Do() comment for what errors you might get.
|
2014-06-23 00:02:48 +00:00
|
|
|
func (r Result) Error() error {
|
|
|
|
return r.err
|
2014-06-22 19:05:34 +00:00
|
|
|
}
|