Move restclient to its own package

This commit is contained in:
Kris
2016-02-12 10:58:43 -08:00
parent 1dd3cb58f8
commit e664ef922f
157 changed files with 1331 additions and 1178 deletions

View File

@@ -17,27 +17,16 @@ limitations under the License.
package unversioned
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path"
"reflect"
gruntime "runtime"
"strings"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/apis/autoscaling"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/version"
)
@@ -47,106 +36,16 @@ const (
defaultAPIPath = "/apis"
)
// Config holds the common attributes that can be passed to a Kubernetes client on
// initialization.
type Config struct {
// Host must be a host string, a host:port pair, or a URL to the base of the apiserver.
// If a URL is given then the (optional) Path of that URL represents a prefix that must
// be appended to all request URIs used to access the apiserver. This allows a frontend
// proxy to easily relocate all of the apiserver endpoints.
Host string
// APIPath is a sub-path that points to an API root.
APIPath string
// Prefix is the sub path of the server. If not specified, the client will set
// a default value. Use "/" to indicate the server root should be used
Prefix string
// ContentConfig contains settings that affect how objects are transformed when
// sent to the server.
ContentConfig
// Server requires Basic authentication
Username string
Password string
// Server requires Bearer authentication. This client will not attempt to use
// refresh tokens for an OAuth2 flow.
// TODO: demonstrate an OAuth2 compatible client.
BearerToken string
// TLSClientConfig contains settings to enable transport layer security
TLSClientConfig
// Server should be accessed without verifying the TLS
// certificate. For testing only.
Insecure bool
// UserAgent is an optional field that specifies the caller of this request.
UserAgent string
// Transport may be used for custom HTTP behavior. This attribute may not
// be specified with the TLS client certificate options. Use WrapTransport
// for most client level operations.
Transport http.RoundTripper
// WrapTransport will be invoked for custom HTTP behavior after the underlying
// transport is initialized (either the transport created from TLSClientConfig,
// Transport, or http.DefaultTransport). The config may layer other RoundTrippers
// on top of the returned RoundTripper.
WrapTransport func(rt http.RoundTripper) http.RoundTripper
// QPS indicates the maximum QPS to the master from this client. If zero, QPS is unlimited.
QPS float32
// Maximum burst for throttle
Burst int
}
// TLSClientConfig contains settings to enable transport layer security
type TLSClientConfig struct {
// Server requires TLS client certificate authentication
CertFile string
// Server requires TLS client certificate authentication
KeyFile string
// Trusted root certificates for server
CAFile string
// CertData holds PEM-encoded bytes (typically read from a client certificate file).
// CertData takes precedence over CertFile
CertData []byte
// KeyData holds PEM-encoded bytes (typically read from a client certificate key file).
// KeyData takes precedence over KeyFile
KeyData []byte
// CAData holds PEM-encoded bytes (typically read from a root certificates bundle).
// CAData takes precedence over CAFile
CAData []byte
}
type ContentConfig struct {
// ContentType specifies the wire format used to communicate with the server.
// This value will be set as the Accept header on requests made to the server, and
// as the default content type on any object sent to the server. If not set,
// "application/json" is used.
ContentType string
// GroupVersion is the API version to talk to. Must be provided when initializing
// a RESTClient directly. When initializing a Client, will be set with the default
// code version.
GroupVersion *unversioned.GroupVersion
// Codec specifies the encoding and decoding behavior for runtime.Objects passed
// to a RESTClient or Client. Required when initializing a RESTClient, optional
// when initializing a Client.
Codec runtime.Codec
}
// New creates a Kubernetes client for the given config. This client works with pods,
// replication controllers, daemons, and services. It allows operations such as list, get, update
// and delete on these objects. An error is returned if the provided configuration
// is not valid.
func New(c *Config) (*Client, error) {
func New(c *restclient.Config) (*Client, error) {
config := *c
if err := SetKubernetesDefaults(&config); err != nil {
return nil, err
}
client, err := RESTClientFor(&config)
client, err := restclient.RESTClientFor(&config)
if err != nil {
return nil, err
}
@@ -190,7 +89,7 @@ func New(c *Config) (*Client, error) {
// MatchesServerVersion queries the server to compares the build version
// (git hash) of the client with the server's build version. It returns an error
// if it failed to contact the server or if the versions are not an exact match.
func MatchesServerVersion(client *Client, c *Config) error {
func MatchesServerVersion(client *Client, c *restclient.Config) error {
var err error
if client == nil {
client, err = New(c)
@@ -220,56 +119,6 @@ func ExtractGroupVersions(l *unversioned.APIGroupList) []string {
return groupVersions
}
// ServerAPIVersions returns the GroupVersions supported by the API server.
// It creates a RESTClient based on the passed in config, but it doesn't rely
// on the Version and Codec of the config, because it uses AbsPath and
// takes the raw response.
func ServerAPIVersions(c *Config) (groupVersions []string, err error) {
transport, err := TransportFor(c)
if err != nil {
return nil, err
}
client := http.Client{Transport: transport}
configCopy := *c
configCopy.GroupVersion = nil
configCopy.APIPath = ""
baseURL, _, err := defaultServerUrlFor(&configCopy)
if err != nil {
return nil, err
}
// Get the groupVersions exposed at /api
originalPath := baseURL.Path
baseURL.Path = path.Join(originalPath, legacyAPIPath)
resp, err := client.Get(baseURL.String())
if err != nil {
return nil, err
}
var v unversioned.APIVersions
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&v)
if err != nil {
return nil, fmt.Errorf("unexpected error: %v", err)
}
groupVersions = append(groupVersions, v.Versions...)
// Get the groupVersions exposed at /apis
baseURL.Path = path.Join(originalPath, defaultAPIPath)
resp2, err := client.Get(baseURL.String())
if err != nil {
return nil, err
}
var apiGroupList unversioned.APIGroupList
defer resp2.Body.Close()
err = json.NewDecoder(resp2.Body).Decode(&apiGroupList)
if err != nil {
return nil, fmt.Errorf("unexpected error: %v", err)
}
groupVersions = append(groupVersions, ExtractGroupVersions(&apiGroupList)...)
return groupVersions, nil
}
// NegotiateVersion queries the server's supported api versions to find
// a version that both client and server support.
// - If no version is provided, try registered client versions in order of
@@ -279,7 +128,7 @@ func ServerAPIVersions(c *Config) (groupVersions []string, err error) {
// stderr and try client's registered versions in order of preference.
// - If version is config default, and the server does not support it,
// return an error.
func NegotiateVersion(client *Client, c *Config, requestedGV *unversioned.GroupVersion, clientRegisteredGVs []unversioned.GroupVersion) (*unversioned.GroupVersion, error) {
func NegotiateVersion(client *Client, c *restclient.Config, requestedGV *unversioned.GroupVersion, clientRegisteredGVs []unversioned.GroupVersion) (*unversioned.GroupVersion, error) {
var err error
if client == nil {
client, err = New(c)
@@ -349,7 +198,7 @@ func NegotiateVersion(client *Client, c *Config, requestedGV *unversioned.GroupV
}
// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized.
func NewOrDie(c *Config) *Client {
func NewOrDie(c *restclient.Config) *Client {
client, err := New(c)
if err != nil {
panic(err)
@@ -357,39 +206,9 @@ func NewOrDie(c *Config) *Client {
return client
}
// InClusterConfig returns a config object which uses the service account
// kubernetes gives to pods. It's intended for clients that expect to be
// running inside a pod running on kuberenetes. It will return an error if
// called from a process not running in a kubernetes environment.
func InClusterConfig() (*Config, error) {
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
if len(host) == 0 || len(port) == 0 {
return nil, fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
}
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/" + api.ServiceAccountTokenKey)
if err != nil {
return nil, err
}
tlsClientConfig := TLSClientConfig{}
rootCAFile := "/var/run/secrets/kubernetes.io/serviceaccount/" + api.ServiceAccountRootCAKey
if _, err := util.CertPoolFromFile(rootCAFile); err != nil {
glog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)
} else {
tlsClientConfig.CAFile = rootCAFile
}
return &Config{
// TODO: switch to using cluster DNS.
Host: "https://" + net.JoinHostPort(host, port),
BearerToken: string(token),
TLSClientConfig: tlsClientConfig,
}, nil
}
// NewInCluster is a shortcut for calling InClusterConfig() and then New().
func NewInCluster() (*Client, error) {
cc, err := InClusterConfig()
cc, err := restclient.InClusterConfig()
if err != nil {
return nil, err
}
@@ -399,13 +218,10 @@ func NewInCluster() (*Client, error) {
// SetKubernetesDefaults sets default values on the provided client config for accessing the
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
// TODO: this method needs to be split into one that sets defaults per group, expected to be fix in PR "Refactoring clientcache.go and helper.go #14592"
func SetKubernetesDefaults(config *Config) error {
func SetKubernetesDefaults(config *restclient.Config) error {
if config.APIPath == "" {
config.APIPath = legacyAPIPath
}
if len(config.UserAgent) == 0 {
config.UserAgent = DefaultKubernetesUserAgent()
}
g, err := registered.Group(api.GroupName)
if err != nil {
return err
@@ -416,233 +232,6 @@ func SetKubernetesDefaults(config *Config) error {
if config.Codec == nil {
config.Codec = api.Codecs.LegacyCodec(*config.GroupVersion)
}
if config.QPS == 0.0 {
config.QPS = 5.0
}
if config.Burst == 0 {
config.Burst = 10
}
return nil
}
// RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config
// object. Note that a RESTClient may require fields that are optional when initializing a Client.
// A RESTClient created by this method is generic - it expects to operate on an API that follows
// the Kubernetes conventions, but may not be the Kubernetes API.
func RESTClientFor(config *Config) (*RESTClient, error) {
if config.GroupVersion == nil {
return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient")
}
if config.Codec == nil {
return nil, fmt.Errorf("Codec is required when initializing a RESTClient")
}
baseURL, versionedAPIPath, err := defaultServerUrlFor(config)
if err != nil {
return nil, err
}
transport, err := TransportFor(config)
if err != nil {
return nil, err
}
var httpClient *http.Client
if transport != http.DefaultTransport {
httpClient = &http.Client{Transport: transport}
}
client := NewRESTClient(baseURL, versionedAPIPath, config.ContentConfig, config.QPS, config.Burst, httpClient)
return client, nil
}
// UnversionedRESTClientFor is the same as RESTClientFor, except that it allows
// the config.Version to be empty.
func UnversionedRESTClientFor(config *Config) (*RESTClient, error) {
if config.Codec == nil {
return nil, fmt.Errorf("Codec is required when initializing a RESTClient")
}
baseURL, versionedAPIPath, err := defaultServerUrlFor(config)
if err != nil {
return nil, err
}
transport, err := TransportFor(config)
if err != nil {
return nil, err
}
var httpClient *http.Client
if transport != http.DefaultTransport {
httpClient = &http.Client{Transport: transport}
}
versionConfig := config.ContentConfig
if versionConfig.GroupVersion == nil {
v := unversioned.SchemeGroupVersion
versionConfig.GroupVersion = &v
}
client := NewRESTClient(baseURL, versionedAPIPath, versionConfig, config.QPS, config.Burst, httpClient)
return client, nil
}
// DefaultServerURL converts a host, host:port, or URL string to the default base server API path
// to use with a Client at a given API version following the standard conventions for a
// Kubernetes API.
func DefaultServerURL(host, apiPath string, groupVersion unversioned.GroupVersion, defaultTLS bool) (*url.URL, string, error) {
if host == "" {
return nil, "", fmt.Errorf("host must be a URL or a host:port pair")
}
base := host
hostURL, err := url.Parse(base)
if err != nil {
return nil, "", err
}
if hostURL.Scheme == "" {
scheme := "http://"
if defaultTLS {
scheme = "https://"
}
hostURL, err = url.Parse(scheme + base)
if err != nil {
return nil, "", err
}
if hostURL.Path != "" && hostURL.Path != "/" {
return nil, "", fmt.Errorf("host must be a URL or a host:port pair: %q", base)
}
}
// hostURL.Path is optional; a non-empty Path is treated as a prefix that is to be applied to
// all URIs used to access the host. this is useful when there's a proxy in front of the
// apiserver that has relocated the apiserver endpoints, forwarding all requests from, for
// example, /a/b/c to the apiserver. in this case the Path should be /a/b/c.
//
// if running without a frontend proxy (that changes the location of the apiserver), then
// hostURL.Path should be blank.
//
// versionedAPIPath, a path relative to baseURL.Path, points to a versioned API base
versionedAPIPath := path.Join("/", apiPath)
// Add the version to the end of the path
if len(groupVersion.Group) > 0 {
versionedAPIPath = path.Join(versionedAPIPath, groupVersion.Group, groupVersion.Version)
} else {
versionedAPIPath = path.Join(versionedAPIPath, groupVersion.Version)
}
return hostURL, versionedAPIPath, nil
}
// IsConfigTransportTLS returns true if and only if the provided config will result in a protected
// connection to the server when it is passed to client.New() or client.RESTClientFor().
// Use to determine when to send credentials over the wire.
//
// Note: the Insecure flag is ignored when testing for this value, so MITM attacks are
// still possible.
func IsConfigTransportTLS(config Config) bool {
// determination of TLS transport does not logically require a version to be specified
// modify the copy of the config we got to satisfy preconditions for defaultServerUrlFor
config.GroupVersion = defaultVersionFor(&config)
baseURL, _, err := defaultServerUrlFor(&config)
if err != nil {
return false
}
return baseURL.Scheme == "https"
}
// defaultServerUrlFor is shared between IsConfigTransportTLS and RESTClientFor. It
// requires Host and Version to be set prior to being called.
func defaultServerUrlFor(config *Config) (*url.URL, string, error) {
// TODO: move the default to secure when the apiserver supports TLS by default
// config.Insecure is taken to mean "I want HTTPS but don't bother checking the certs against a CA."
hasCA := len(config.CAFile) != 0 || len(config.CAData) != 0
hasCert := len(config.CertFile) != 0 || len(config.CertData) != 0
defaultTLS := hasCA || hasCert || config.Insecure
host := config.Host
if host == "" {
host = "localhost"
}
if config.GroupVersion != nil {
return DefaultServerURL(host, config.APIPath, *config.GroupVersion, defaultTLS)
}
return DefaultServerURL(host, config.APIPath, unversioned.GroupVersion{}, defaultTLS)
}
// defaultVersionFor is shared between IsConfigTransportTLS and RESTClientFor
func defaultVersionFor(config *Config) *unversioned.GroupVersion {
if config.GroupVersion == nil {
// Clients default to the preferred code API version
// TODO: implement version negotiation (highest version supported by server)
// TODO this drops out when groupmeta is refactored
copyGroupVersion := registered.GroupOrDie(api.GroupName).GroupVersion
return &copyGroupVersion
}
return config.GroupVersion
}
// DefaultKubernetesUserAgent returns the default user agent that clients can use.
func DefaultKubernetesUserAgent() string {
commit := version.Get().GitCommit
if len(commit) > 7 {
commit = commit[:7]
}
if len(commit) == 0 {
commit = "unknown"
}
version := version.Get().GitVersion
seg := strings.SplitN(version, "-", 2)
version = seg[0]
return fmt.Sprintf("%s/%s (%s/%s) kubernetes/%s", path.Base(os.Args[0]), version, gruntime.GOOS, gruntime.GOARCH, commit)
}
// LoadTLSFiles copies the data from the CertFile, KeyFile, and CAFile fields into the CertData,
// KeyData, and CAFile fields, or returns an error. If no error is returned, all three fields are
// either populated or were empty to start.
func LoadTLSFiles(c *Config) error {
var err error
c.CAData, err = dataFromSliceOrFile(c.CAData, c.CAFile)
if err != nil {
return err
}
c.CertData, err = dataFromSliceOrFile(c.CertData, c.CertFile)
if err != nil {
return err
}
c.KeyData, err = dataFromSliceOrFile(c.KeyData, c.KeyFile)
if err != nil {
return err
}
return nil
}
// dataFromSliceOrFile returns data from the slice (if non-empty), or from the file,
// or an error if an error occurred reading the file
func dataFromSliceOrFile(data []byte, file string) ([]byte, error) {
if len(data) > 0 {
return data, nil
}
if len(file) > 0 {
fileData, err := ioutil.ReadFile(file)
if err != nil {
return []byte{}, err
}
return fileData, nil
}
return nil, nil
}
func AddUserAgent(config *Config, userAgent string) *Config {
fullUserAgent := DefaultKubernetesUserAgent() + "/" + userAgent
config.UserAgent = fullUserAgent
return config
return restclient.SetKubernetesDefaults(config)
}