2020-10-30 16:30:03 +01:00
/ *
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"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
gruntime "runtime"
"strings"
"time"
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"
"k8s.io/client-go/transport"
certutil "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/flowcontrol"
2020-12-25 09:00:08 +01:00
"k8s.io/klog"
2020-10-30 16:30:03 +01:00
)
const (
DefaultQPS float32 = 5.0
DefaultBurst int = 10
)
var ErrNotInCluster = errors . New ( "unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined" )
// 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
// Path to a file containing a BearerToken.
// If set, the contents are periodically read.
// The last successfully read value takes precedence over BearerToken.
BearerTokenFile 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
2020-12-25 06:07:18 +01:00
// DisableCompression bypasses automatic GZip compression requests to the
// server.
DisableCompression bool
2020-10-30 16:30:03 +01:00
// Transport may be used for custom HTTP behavior. This attribute may not
// be specified with the TLS client certificate options. Use WrapTransport
// to provide additional per-server middleware behavior.
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.
//
// A future release will change this field to an array. Use config.Wrap()
// instead of setting this value directly.
WrapTransport transport . WrapperFunc
// 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
}
var _ fmt . Stringer = new ( Config )
var _ fmt . GoStringer = new ( Config )
type sanitizedConfig * Config
type sanitizedAuthConfigPersister struct { AuthProviderConfigPersister }
func ( sanitizedAuthConfigPersister ) GoString ( ) string {
return "rest.AuthProviderConfigPersister(--- REDACTED ---)"
}
func ( sanitizedAuthConfigPersister ) String ( ) string {
return "rest.AuthProviderConfigPersister(--- REDACTED ---)"
}
// GoString implements fmt.GoStringer and sanitizes sensitive fields of Config
// to prevent accidental leaking via logs.
func ( c * Config ) GoString ( ) string {
return c . String ( )
}
// String implements fmt.Stringer and sanitizes sensitive fields of Config to
// prevent accidental leaking via logs.
func ( c * Config ) String ( ) string {
if c == nil {
return "<nil>"
}
cc := sanitizedConfig ( CopyConfig ( c ) )
// Explicitly mark non-empty credential fields as redacted.
if cc . Password != "" {
cc . Password = "--- REDACTED ---"
}
if cc . BearerToken != "" {
cc . BearerToken = "--- REDACTED ---"
}
if cc . AuthConfigPersister != nil {
cc . AuthConfigPersister = sanitizedAuthConfigPersister { cc . AuthConfigPersister }
}
return fmt . Sprintf ( "%#v" , cc )
}
// 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
2020-12-25 06:07:18 +01:00
// NextProtos is a list of supported application level protocols, in order of preference.
// Used to populate tls.Config.NextProtos.
// To indicate to the server http/1.1 is preferred over http/2, set to ["http/1.1", "h2"] (though the server is free to ignore that preference).
// To use only http/1.1, set to ["http/1.1"].
NextProtos [ ] string
2020-10-30 16:30:03 +01:00
}
var _ fmt . Stringer = TLSClientConfig { }
var _ fmt . GoStringer = TLSClientConfig { }
type sanitizedTLSClientConfig TLSClientConfig
// GoString implements fmt.GoStringer and sanitizes sensitive fields of
// TLSClientConfig to prevent accidental leaking via logs.
func ( c TLSClientConfig ) GoString ( ) string {
return c . String ( )
}
// String implements fmt.Stringer and sanitizes sensitive fields of
// TLSClientConfig to prevent accidental leaking via logs.
func ( c TLSClientConfig ) String ( ) string {
cc := sanitizedTLSClientConfig {
Insecure : c . Insecure ,
ServerName : c . ServerName ,
CertFile : c . CertFile ,
KeyFile : c . KeyFile ,
CAFile : c . CAFile ,
CertData : c . CertData ,
KeyData : c . KeyData ,
CAData : c . CAData ,
2020-12-25 06:07:18 +01:00
NextProtos : c . NextProtos ,
2020-10-30 16:30:03 +01:00
}
// Explicitly mark non-empty credential fields as redacted.
if len ( cc . CertData ) != 0 {
cc . CertData = [ ] byte ( "--- TRUNCATED ---" )
}
if len ( cc . KeyData ) != 0 {
cc . KeyData = [ ] byte ( "--- REDACTED ---" )
}
return fmt . Sprintf ( "%#v" , cc )
}
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.
2020-12-25 06:07:18 +01:00
//
// TODO: NegotiatedSerializer will be phased out as internal clients are removed
// from Kubernetes.
2020-10-30 16:30:03 +01:00
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" )
}
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
}
}
2020-12-25 06:07:18 +01:00
rateLimiter := config . RateLimiter
if rateLimiter == nil {
qps := config . QPS
if config . QPS == 0.0 {
qps = DefaultQPS
}
burst := config . Burst
if config . Burst == 0 {
burst = DefaultBurst
}
if qps > 0 {
rateLimiter = flowcontrol . NewTokenBucketRateLimiter ( qps , burst )
}
}
var gv schema . GroupVersion
if config . GroupVersion != nil {
gv = * config . GroupVersion
}
clientContent := ClientContentConfig {
AcceptContentTypes : config . AcceptContentTypes ,
ContentType : config . ContentType ,
GroupVersion : gv ,
Negotiator : runtime . NewClientNegotiator ( config . NegotiatedSerializer , gv ) ,
}
2020-12-25 09:00:08 +01:00
return NewRESTClient ( baseURL , versionedAPIPath , clientContent , rateLimiter , httpClient )
2020-10-30 16:30:03 +01:00
}
// 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 ( "NegotiatedSerializer 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
}
}
2020-12-25 06:07:18 +01:00
rateLimiter := config . RateLimiter
if rateLimiter == nil {
qps := config . QPS
if config . QPS == 0.0 {
qps = DefaultQPS
}
burst := config . Burst
if config . Burst == 0 {
burst = DefaultBurst
}
if qps > 0 {
rateLimiter = flowcontrol . NewTokenBucketRateLimiter ( qps , burst )
}
}
gv := metav1 . SchemeGroupVersion
if config . GroupVersion != nil {
gv = * config . GroupVersion
}
clientContent := ClientContentConfig {
AcceptContentTypes : config . AcceptContentTypes ,
ContentType : config . ContentType ,
GroupVersion : gv ,
Negotiator : runtime . NewClientNegotiator ( config . NegotiatedSerializer , gv ) ,
2020-10-30 16:30:03 +01:00
}
2020-12-25 09:00:08 +01:00
return NewRESTClient ( baseURL , versionedAPIPath , clientContent , rateLimiter , httpClient )
2020-10-30 16:30:03 +01:00
}
// 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 ErrNotInCluster
// if called from a process not running in a kubernetes environment.
func InClusterConfig ( ) ( * Config , error ) {
const (
tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
host , port := os . Getenv ( "KUBERNETES_SERVICE_HOST" ) , os . Getenv ( "KUBERNETES_SERVICE_PORT" )
if len ( host ) == 0 || len ( port ) == 0 {
return nil , ErrNotInCluster
}
token , err := ioutil . ReadFile ( tokenFile )
if err != nil {
return nil , err
}
tlsClientConfig := TLSClientConfig { }
if _ , err := certutil . NewPool ( rootCAFile ) ; err != nil {
klog . 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 ) ,
TLSClientConfig : tlsClientConfig ,
BearerToken : string ( token ) ,
BearerTokenFile : tokenFile ,
} , 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
}
2020-12-25 06:07:18 +01:00
// AnonymousClientConfig returns a copy of the given config with all user credentials (cert/key, bearer token, and username/password) and custom transports (WrapTransport, Transport) removed
2020-10-30 16:30:03 +01:00
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 ,
2020-12-25 06:07:18 +01:00
NextProtos : config . TLSClientConfig . NextProtos ,
2020-10-30 16:30:03 +01:00
} ,
2020-12-25 06:07:18 +01:00
RateLimiter : config . RateLimiter ,
UserAgent : config . UserAgent ,
DisableCompression : config . DisableCompression ,
QPS : config . QPS ,
Burst : config . Burst ,
Timeout : config . Timeout ,
Dial : config . Dial ,
2020-10-30 16:30:03 +01:00
}
}
// 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 ,
BearerTokenFile : config . BearerTokenFile ,
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 ,
2020-12-25 06:07:18 +01:00
NextProtos : config . TLSClientConfig . NextProtos ,
2020-10-30 16:30:03 +01:00
} ,
2020-12-25 06:07:18 +01:00
UserAgent : config . UserAgent ,
DisableCompression : config . DisableCompression ,
Transport : config . Transport ,
WrapTransport : config . WrapTransport ,
QPS : config . QPS ,
Burst : config . Burst ,
RateLimiter : config . RateLimiter ,
Timeout : config . Timeout ,
Dial : config . Dial ,
2020-10-30 16:30:03 +01:00
}
}