You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
458 lines
15 KiB
458 lines
15 KiB
/* |
|
Copyright 2016 The Kubernetes Authors. |
|
|
|
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. |
|
*/ |
|
|
|
package rest |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"io/ioutil" |
|
"net" |
|
"net/http" |
|
"os" |
|
"path/filepath" |
|
gruntime "runtime" |
|
"strings" |
|
"time" |
|
|
|
"github.com/golang/glog" |
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
|
"k8s.io/apimachinery/pkg/runtime" |
|
"k8s.io/apimachinery/pkg/runtime/schema" |
|
"k8s.io/client-go/pkg/version" |
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" |
|
certutil "k8s.io/client-go/util/cert" |
|
"k8s.io/client-go/util/flowcontrol" |
|
) |
|
|
|
const ( |
|
DefaultQPS float32 = 5.0 |
|
DefaultBurst int = 10 |
|
) |
|
|
|
// 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 |
|
|
|
// 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 |
|
|
|
// Impersonate is the configuration that RESTClient will use for impersonation. |
|
Impersonate ImpersonationConfig |
|
|
|
// Server requires plugin-specified authentication. |
|
AuthProvider *clientcmdapi.AuthProviderConfig |
|
|
|
// Callback to persist config for AuthProvider. |
|
AuthConfigPersister AuthProviderConfigPersister |
|
|
|
// Exec-based authentication provider. |
|
ExecProvider *clientcmdapi.ExecConfig |
|
|
|
// TLSClientConfig contains settings to enable transport layer security |
|
TLSClientConfig |
|
|
|
// 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 it's zero, the created RESTClient will use DefaultQPS: 5 |
|
QPS float32 |
|
|
|
// Maximum burst for throttle. |
|
// If it's zero, the created RESTClient will use DefaultBurst: 10. |
|
Burst int |
|
|
|
// Rate limiter for limiting connections to the master from this client. If present overwrites QPS/Burst |
|
RateLimiter flowcontrol.RateLimiter |
|
|
|
// The maximum length of time to wait before giving up on a server request. A value of zero means no timeout. |
|
Timeout time.Duration |
|
|
|
// Dial specifies the dial function for creating unencrypted TCP connections. |
|
Dial func(ctx context.Context, network, address string) (net.Conn, error) |
|
|
|
// Version forces a specific version to be used (if registered) |
|
// Do we need this? |
|
// Version string |
|
} |
|
|
|
// ImpersonationConfig has all the available impersonation options |
|
type ImpersonationConfig struct { |
|
// UserName is the username to impersonate on each request. |
|
UserName string |
|
// Groups are the groups to impersonate on each request. |
|
Groups []string |
|
// Extra is a free-form field which can be used to link some authentication information |
|
// to authorization information. This field allows you to impersonate it. |
|
Extra map[string][]string |
|
} |
|
|
|
// +k8s:deepcopy-gen=true |
|
// TLSClientConfig contains settings to enable transport layer security |
|
type TLSClientConfig struct { |
|
// Server should be accessed without verifying the TLS certificate. For testing only. |
|
Insecure bool |
|
// ServerName is passed to the server for SNI and is used in the client to check server |
|
// ceritificates against. If ServerName is empty, the hostname used to contact the |
|
// server is used. |
|
ServerName string |
|
|
|
// 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 { |
|
// AcceptContentTypes specifies the types the client will accept and is optional. |
|
// If not set, ContentType will be used to define the Accept header |
|
AcceptContentTypes string |
|
// 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 *schema.GroupVersion |
|
// NegotiatedSerializer is used for obtaining encoders and decoders for multiple |
|
// supported media types. |
|
NegotiatedSerializer runtime.NegotiatedSerializer |
|
} |
|
|
|
// 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.NegotiatedSerializer == nil { |
|
return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient") |
|
} |
|
qps := config.QPS |
|
if config.QPS == 0.0 { |
|
qps = DefaultQPS |
|
} |
|
burst := config.Burst |
|
if config.Burst == 0 { |
|
burst = DefaultBurst |
|
} |
|
|
|
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} |
|
if config.Timeout > 0 { |
|
httpClient.Timeout = config.Timeout |
|
} |
|
} |
|
|
|
return NewRESTClient(baseURL, versionedAPIPath, config.ContentConfig, qps, burst, config.RateLimiter, httpClient) |
|
} |
|
|
|
// UnversionedRESTClientFor is the same as RESTClientFor, except that it allows |
|
// the config.Version to be empty. |
|
func UnversionedRESTClientFor(config *Config) (*RESTClient, error) { |
|
if config.NegotiatedSerializer == nil { |
|
return nil, fmt.Errorf("NeogitatedSerializer 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} |
|
if config.Timeout > 0 { |
|
httpClient.Timeout = config.Timeout |
|
} |
|
} |
|
|
|
versionConfig := config.ContentConfig |
|
if versionConfig.GroupVersion == nil { |
|
v := metav1.SchemeGroupVersion |
|
versionConfig.GroupVersion = &v |
|
} |
|
|
|
return NewRESTClient(baseURL, versionedAPIPath, versionConfig, config.QPS, config.Burst, config.RateLimiter, httpClient) |
|
} |
|
|
|
// 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. |
|
func SetKubernetesDefaults(config *Config) error { |
|
if len(config.UserAgent) == 0 { |
|
config.UserAgent = DefaultKubernetesUserAgent() |
|
} |
|
return nil |
|
} |
|
|
|
// adjustCommit returns sufficient significant figures of the commit's git hash. |
|
func adjustCommit(c string) string { |
|
if len(c) == 0 { |
|
return "unknown" |
|
} |
|
if len(c) > 7 { |
|
return c[:7] |
|
} |
|
return c |
|
} |
|
|
|
// adjustVersion strips "alpha", "beta", etc. from version in form |
|
// major.minor.patch-[alpha|beta|etc]. |
|
func adjustVersion(v string) string { |
|
if len(v) == 0 { |
|
return "unknown" |
|
} |
|
seg := strings.SplitN(v, "-", 2) |
|
return seg[0] |
|
} |
|
|
|
// adjustCommand returns the last component of the |
|
// OS-specific command path for use in User-Agent. |
|
func adjustCommand(p string) string { |
|
// Unlikely, but better than returning "". |
|
if len(p) == 0 { |
|
return "unknown" |
|
} |
|
return filepath.Base(p) |
|
} |
|
|
|
// buildUserAgent builds a User-Agent string from given args. |
|
func buildUserAgent(command, version, os, arch, commit string) string { |
|
return fmt.Sprintf( |
|
"%s/%s (%s/%s) kubernetes/%s", command, version, os, arch, commit) |
|
} |
|
|
|
// DefaultKubernetesUserAgent returns a User-Agent string built from static global vars. |
|
func DefaultKubernetesUserAgent() string { |
|
return buildUserAgent( |
|
adjustCommand(os.Args[0]), |
|
adjustVersion(version.Get().GitVersion), |
|
gruntime.GOOS, |
|
gruntime.GOARCH, |
|
adjustCommit(version.Get().GitCommit)) |
|
} |
|
|
|
// 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 kubernetes. 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/token") |
|
if err != nil { |
|
return nil, err |
|
} |
|
tlsClientConfig := TLSClientConfig{} |
|
rootCAFile := "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" |
|
if _, err := certutil.NewPool(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 |
|
} |
|
|
|
// IsConfigTransportTLS returns true if and only if the provided |
|
// config will result in a protected connection to the server when it |
|
// is passed to restclient.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 { |
|
baseURL, _, err := defaultServerUrlFor(&config) |
|
if err != nil { |
|
return false |
|
} |
|
return baseURL.Scheme == "https" |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// AnonymousClientConfig returns a copy of the given config with all user credentials (cert/key, bearer token, and username/password) removed |
|
func AnonymousClientConfig(config *Config) *Config { |
|
// copy only known safe fields |
|
return &Config{ |
|
Host: config.Host, |
|
APIPath: config.APIPath, |
|
ContentConfig: config.ContentConfig, |
|
TLSClientConfig: TLSClientConfig{ |
|
Insecure: config.Insecure, |
|
ServerName: config.ServerName, |
|
CAFile: config.TLSClientConfig.CAFile, |
|
CAData: config.TLSClientConfig.CAData, |
|
}, |
|
RateLimiter: config.RateLimiter, |
|
UserAgent: config.UserAgent, |
|
Transport: config.Transport, |
|
WrapTransport: config.WrapTransport, |
|
QPS: config.QPS, |
|
Burst: config.Burst, |
|
Timeout: config.Timeout, |
|
Dial: config.Dial, |
|
} |
|
} |
|
|
|
// CopyConfig returns a copy of the given config |
|
func CopyConfig(config *Config) *Config { |
|
return &Config{ |
|
Host: config.Host, |
|
APIPath: config.APIPath, |
|
ContentConfig: config.ContentConfig, |
|
Username: config.Username, |
|
Password: config.Password, |
|
BearerToken: config.BearerToken, |
|
Impersonate: ImpersonationConfig{ |
|
Groups: config.Impersonate.Groups, |
|
Extra: config.Impersonate.Extra, |
|
UserName: config.Impersonate.UserName, |
|
}, |
|
AuthProvider: config.AuthProvider, |
|
AuthConfigPersister: config.AuthConfigPersister, |
|
ExecProvider: config.ExecProvider, |
|
TLSClientConfig: TLSClientConfig{ |
|
Insecure: config.TLSClientConfig.Insecure, |
|
ServerName: config.TLSClientConfig.ServerName, |
|
CertFile: config.TLSClientConfig.CertFile, |
|
KeyFile: config.TLSClientConfig.KeyFile, |
|
CAFile: config.TLSClientConfig.CAFile, |
|
CertData: config.TLSClientConfig.CertData, |
|
KeyData: config.TLSClientConfig.KeyData, |
|
CAData: config.TLSClientConfig.CAData, |
|
}, |
|
UserAgent: config.UserAgent, |
|
Transport: config.Transport, |
|
WrapTransport: config.WrapTransport, |
|
QPS: config.QPS, |
|
Burst: config.Burst, |
|
RateLimiter: config.RateLimiter, |
|
Timeout: config.Timeout, |
|
Dial: config.Dial, |
|
} |
|
}
|
|
|