package openshift import ( "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "path" "path/filepath" "reflect" "strings" "time" "github.com/ghodss/yaml" "github.com/imdario/mergo" "github.com/pkg/errors" "golang.org/x/net/http2" "k8s.io/client-go/util/homedir" ) // restTLSClientConfig is a modified copy of k8s.io/kubernets/pkg/client/restclient.TLSClientConfig. // restTLSClientConfig contains settings to enable transport layer security type restTLSClientConfig 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 } // restConfig is a modified copy of k8s.io/kubernets/pkg/client/restclient.Config. // Config holds the common attributes that can be passed to a Kubernetes client on // initialization. type restConfig 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 // 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 restTLSClientConfig // Server should be accessed without verifying the TLS // certificate. For testing only. Insecure bool } // ClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfig. // ClientConfig is used to make it easy to get an api server client type clientConfig interface { // ClientConfig returns a complete client config ClientConfig() (*restConfig, error) } // defaultClientConfig is a modified copy of openshift/origin/pkg/cmd/util/clientcmd.DefaultClientConfig. func defaultClientConfig() clientConfig { loadingRules := newOpenShiftClientConfigLoadingRules() // REMOVED: Allowing command-line overriding of loadingRules // REMOVED: clientcmd.ConfigOverrides clientConfig := newNonInteractiveDeferredLoadingClientConfig(loadingRules) return clientConfig } var recommendedHomeFile = path.Join(homedir.HomeDir(), ".kube/config") // newOpenShiftClientConfigLoadingRules is a modified copy of openshift/origin/pkg/cmd/cli/config.NewOpenShiftClientConfigLoadingRules. // NewOpenShiftClientConfigLoadingRules returns file priority loading rules for OpenShift. // 1. --config value // 2. if KUBECONFIG env var has a value, use it. Otherwise, ~/.kube/config file func newOpenShiftClientConfigLoadingRules() *clientConfigLoadingRules { chain := []string{} envVarFile := os.Getenv("KUBECONFIG") if len(envVarFile) != 0 { chain = append(chain, filepath.SplitList(envVarFile)...) } else { chain = append(chain, recommendedHomeFile) } return &clientConfigLoadingRules{ Precedence: chain, // REMOVED: Migration support; run (oc login) to trigger migration } } // deferredLoadingClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DeferredLoadingClientConfig. // DeferredLoadingClientConfig is a ClientConfig interface that is backed by a set of loading rules // It is used in cases where the loading rules may change after you've instantiated them and you want to be sure that // the most recent rules are used. This is useful in cases where you bind flags to loading rule parameters before // the parse happens and you want your calling code to be ignorant of how the values are being mutated to avoid // passing extraneous information down a call stack type deferredLoadingClientConfig struct { loadingRules *clientConfigLoadingRules clientConfig clientConfig } // NewNonInteractiveDeferredLoadingClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.NewNonInteractiveDeferredLoadingClientConfig. // NewNonInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name func newNonInteractiveDeferredLoadingClientConfig(loadingRules *clientConfigLoadingRules) clientConfig { return &deferredLoadingClientConfig{loadingRules: loadingRules} } func (config *deferredLoadingClientConfig) createClientConfig() (clientConfig, error) { if config.clientConfig == nil { // REMOVED: Support for concurrent use in multiple threads. mergedConfig, err := config.loadingRules.Load() if err != nil { return nil, err } var mergedClientConfig clientConfig // REMOVED: Interactive fallback support. mergedClientConfig = newNonInteractiveClientConfig(*mergedConfig) config.clientConfig = mergedClientConfig } return config.clientConfig, nil } // ClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DeferredLoadingClientConfig.ClientConfig. // ClientConfig implements ClientConfig func (config *deferredLoadingClientConfig) ClientConfig() (*restConfig, error) { mergedClientConfig, err := config.createClientConfig() if err != nil { return nil, err } mergedConfig, err := mergedClientConfig.ClientConfig() if err != nil { return nil, err } // REMOVED: In-cluster service account configuration use. return mergedConfig, nil } var ( // DefaultCluster is the cluster config used when no other config is specified // TODO: eventually apiserver should start on 443 and be secure by default defaultCluster = clientcmdCluster{Server: "http://localhost:8080"} // EnvVarCluster allows overriding the DefaultCluster using an envvar for the server name envVarCluster = clientcmdCluster{Server: os.Getenv("KUBERNETES_MASTER")} ) // directClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig. // DirectClientConfig is a ClientConfig interface that is backed by a clientcmdapi.Config, options overrides, and an optional fallbackReader for auth information type directClientConfig struct { config clientcmdConfig } // newNonInteractiveClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.NewNonInteractiveClientConfig. // NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information func newNonInteractiveClientConfig(config clientcmdConfig) clientConfig { return &directClientConfig{config} } // ClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.ClientConfig. // ClientConfig implements ClientConfig func (config *directClientConfig) ClientConfig() (*restConfig, error) { if err := config.ConfirmUsable(); err != nil { return nil, err } configAuthInfo := config.getAuthInfo() configClusterInfo := config.getCluster() clientConfig := &restConfig{} clientConfig.Host = configClusterInfo.Server if u, err := url.ParseRequestURI(clientConfig.Host); err == nil && u.Opaque == "" && len(u.Path) > 1 { u.RawQuery = "" u.Fragment = "" clientConfig.Host = u.String() } // only try to read the auth information if we are secure if isConfigTransportTLS(*clientConfig) { var err error // mergo is a first write wins for map value and a last writing wins for interface values // NOTE: This behavior changed with https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a. // Our mergo.Merge version is older than this change. // REMOVED: Support for interactive fallback. userAuthPartialConfig, err := getUserIdentificationPartialConfig(configAuthInfo) if err != nil { return nil, err } mergo.Merge(clientConfig, userAuthPartialConfig) serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo) if err != nil { return nil, err } mergo.Merge(clientConfig, serverAuthPartialConfig) } return clientConfig, nil } // getServerIdentificationPartialConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.getServerIdentificationPartialConfig. // clientauth.Info object contain both user identification and server identification. We want different precedence orders for // both, so we have to split the objects and merge them separately // we want this order of precedence for the server identification // 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files) // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. load the ~/.kubernetes_auth file as a default func getServerIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo, configClusterInfo clientcmdCluster) (*restConfig, error) { mergedConfig := &restConfig{} // configClusterInfo holds the information identify the server provided by .kubeconfig configClientConfig := &restConfig{} configClientConfig.CAFile = configClusterInfo.CertificateAuthority configClientConfig.CAData = configClusterInfo.CertificateAuthorityData configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify mergo.Merge(mergedConfig, configClientConfig) return mergedConfig, nil } // getUserIdentificationPartialConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.getUserIdentificationPartialConfig. // clientauth.Info object contain both user identification and server identification. We want different precedence orders for // both, so we have to split the objects and merge them separately // we want this order of precedence for user identifcation // 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files) // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. if there is not enough information to idenfity the user, load try the ~/.kubernetes_auth file // 4. if there is not enough information to identify the user, prompt if possible func getUserIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo) (*restConfig, error) { mergedConfig := &restConfig{} // blindly overwrite existing values based on precedence if len(configAuthInfo.Token) > 0 { mergedConfig.BearerToken = configAuthInfo.Token } if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 { mergedConfig.CertFile = configAuthInfo.ClientCertificate mergedConfig.CertData = configAuthInfo.ClientCertificateData mergedConfig.KeyFile = configAuthInfo.ClientKey mergedConfig.KeyData = configAuthInfo.ClientKeyData } if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 { mergedConfig.Username = configAuthInfo.Username mergedConfig.Password = configAuthInfo.Password } // REMOVED: prompting for missing information. return mergedConfig, nil } // canIdentifyUser is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.canIdentifyUser func canIdentifyUser(config restConfig) bool { return len(config.Username) > 0 || (len(config.CertFile) > 0 || len(config.CertData) > 0) || len(config.BearerToken) > 0 } // ConfirmUsable is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.ConfirmUsable. // ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config, // but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible. func (config *directClientConfig) ConfirmUsable() error { var validationErrors []error validationErrors = append(validationErrors, validateAuthInfo(config.getAuthInfoName(), config.getAuthInfo())...) validationErrors = append(validationErrors, validateClusterInfo(config.getClusterName(), config.getCluster())...) // when direct client config is specified, and our only error is that no server is defined, we should // return a standard "no config" error if len(validationErrors) == 1 && validationErrors[0] == errEmptyCluster { return newErrConfigurationInvalid([]error{errEmptyConfig}) } return newErrConfigurationInvalid(validationErrors) } // getContextName is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.getContextName. func (config *directClientConfig) getContextName() string { // REMOVED: overrides support return config.config.CurrentContext } // getAuthInfoName is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.getAuthInfoName. func (config *directClientConfig) getAuthInfoName() string { // REMOVED: overrides support return config.getContext().AuthInfo } // getClusterName is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.getClusterName. func (config *directClientConfig) getClusterName() string { // REMOVED: overrides support return config.getContext().Cluster } // getContext is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.getContext. func (config *directClientConfig) getContext() clientcmdContext { contexts := config.config.Contexts contextName := config.getContextName() var mergedContext clientcmdContext if configContext, exists := contexts[contextName]; exists { mergo.Merge(&mergedContext, configContext) } // REMOVED: overrides support return mergedContext } var ( errEmptyConfig = errors.New("no configuration has been provided") // message is for consistency with old behavior errEmptyCluster = errors.New("cluster has no server defined") ) // validateClusterInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.validateClusterInfo. // validateClusterInfo looks for conflicts and errors in the cluster info func validateClusterInfo(clusterName string, clusterInfo clientcmdCluster) []error { var validationErrors []error if reflect.DeepEqual(clientcmdCluster{}, clusterInfo) { return []error{errEmptyCluster} } if len(clusterInfo.Server) == 0 { if len(clusterName) == 0 { validationErrors = append(validationErrors, errors.Errorf("default cluster has no server defined")) } else { validationErrors = append(validationErrors, errors.Errorf("no server found for cluster %q", clusterName)) } } // Make sure CA data and CA file aren't both specified if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 { validationErrors = append(validationErrors, errors.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override", clusterName)) } if len(clusterInfo.CertificateAuthority) != 0 { clientCertCA, err := os.Open(clusterInfo.CertificateAuthority) defer clientCertCA.Close() if err != nil { validationErrors = append(validationErrors, errors.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err)) } } return validationErrors } // validateAuthInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.validateAuthInfo. // validateAuthInfo looks for conflicts and errors in the auth info func validateAuthInfo(authInfoName string, authInfo clientcmdAuthInfo) []error { var validationErrors []error usingAuthPath := false methods := make([]string, 0, 3) if len(authInfo.Token) != 0 { methods = append(methods, "token") } if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 { methods = append(methods, "basicAuth") } if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 { // Make sure cert data and file aren't both specified if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 { validationErrors = append(validationErrors, errors.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override", authInfoName)) } // Make sure key data and file aren't both specified if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 { validationErrors = append(validationErrors, errors.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName)) } // Make sure a key is specified if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 { validationErrors = append(validationErrors, errors.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method", authInfoName)) } if len(authInfo.ClientCertificate) != 0 { clientCertFile, err := os.Open(authInfo.ClientCertificate) defer clientCertFile.Close() if err != nil { validationErrors = append(validationErrors, errors.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err)) } } if len(authInfo.ClientKey) != 0 { clientKeyFile, err := os.Open(authInfo.ClientKey) defer clientKeyFile.Close() if err != nil { validationErrors = append(validationErrors, errors.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err)) } } } // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case if (len(methods) > 1) && (!usingAuthPath) { validationErrors = append(validationErrors, errors.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods)) } return validationErrors } // getAuthInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.getAuthInfo. func (config *directClientConfig) getAuthInfo() clientcmdAuthInfo { authInfos := config.config.AuthInfos authInfoName := config.getAuthInfoName() var mergedAuthInfo clientcmdAuthInfo if configAuthInfo, exists := authInfos[authInfoName]; exists { mergo.Merge(&mergedAuthInfo, configAuthInfo) } // REMOVED: overrides support return mergedAuthInfo } // getCluster is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.getCluster. func (config *directClientConfig) getCluster() clientcmdCluster { clusterInfos := config.config.Clusters clusterInfoName := config.getClusterName() var mergedClusterInfo clientcmdCluster mergo.Merge(&mergedClusterInfo, defaultCluster) mergo.Merge(&mergedClusterInfo, envVarCluster) if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists { mergo.Merge(&mergedClusterInfo, configClusterInfo) } // REMOVED: overrides support return mergedClusterInfo } // aggregateErr is a modified copy of k8s.io/apimachinery/pkg/util/errors.aggregate. // This helper implements the error and Errors interfaces. Keeping it private // prevents people from making an aggregate of 0 errors, which is not // an error, but does satisfy the error interface. type aggregateErr []error // newAggregate is a modified copy of k8s.io/apimachinery/pkg/util/errors.NewAggregate. // NewAggregate converts a slice of errors into an Aggregate interface, which // is itself an implementation of the error interface. If the slice is empty, // this returns nil. // It will check if any of the element of input error list is nil, to avoid // nil pointer panic when call Error(). func newAggregate(errlist []error) error { if len(errlist) == 0 { return nil } // In case of input error list contains nil var errs []error for _, e := range errlist { if e != nil { errs = append(errs, e) } } if len(errs) == 0 { return nil } return aggregateErr(errs) } // Error is a modified copy of k8s.io/apimachinery/pkg/util/errors.aggregate.Error. // Error is part of the error interface. func (agg aggregateErr) Error() string { if len(agg) == 0 { // This should never happen, really. return "" } if len(agg) == 1 { return agg[0].Error() } result := fmt.Sprintf("[%s", agg[0].Error()) for i := 1; i < len(agg); i++ { result += fmt.Sprintf(", %s", agg[i].Error()) } result += "]" return result } // REMOVED: aggregateErr.Errors // errConfigurationInvalid is a modified? copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.errConfigurationInvalid. // errConfigurationInvalid is a set of errors indicating the configuration is invalid. type errConfigurationInvalid []error var _ error = errConfigurationInvalid{} // REMOVED: utilerrors.Aggregate implementation for errConfigurationInvalid. // newErrConfigurationInvalid is a modified? copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.newErrConfigurationInvalid. func newErrConfigurationInvalid(errs []error) error { switch len(errs) { case 0: return nil default: return errConfigurationInvalid(errs) } } // Error implements the error interface func (e errConfigurationInvalid) Error() string { return fmt.Sprintf("invalid configuration: %v", newAggregate(e).Error()) } // clientConfigLoadingRules is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules // ClientConfigLoadingRules is an ExplicitPath and string slice of specific locations that are used for merging together a Config // Callers can put the chain together however they want, but we'd recommend: // EnvVarPathFiles if set (a list of files if set) OR the HomeDirectoryPath // ExplicitPath is special, because if a user specifically requests a certain file be used and error is reported if thie file is not present type clientConfigLoadingRules struct { Precedence []string } // Load is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules.Load // Load starts by running the MigrationRules and then // takes the loading rules and returns a Config object based on following rules. // if the ExplicitPath, return the unmerged explicit file // Otherwise, return a merged config based on the Precedence slice // A missing ExplicitPath file produces an error. Empty filenames or other missing files are ignored. // Read errors or files with non-deserializable content produce errors. // The first file to set a particular map key wins and map key's value is never changed. // BUT, if you set a struct value that is NOT contained inside of map, the value WILL be changed. // This results in some odd looking logic to merge in one direction, merge in the other, and then merge the two. // It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even // non-conflicting entries from the second file's "red-user" are discarded. // Relative paths inside of the .kubeconfig files are resolved against the .kubeconfig file's parent folder // and only absolute file paths are returned. func (rules *clientConfigLoadingRules) Load() (*clientcmdConfig, error) { errlist := []error{} kubeConfigFiles := []string{} // REMOVED: explicit path support kubeConfigFiles = append(kubeConfigFiles, rules.Precedence...) kubeconfigs := []*clientcmdConfig{} // read and cache the config files so that we only look at them once for _, filename := range kubeConfigFiles { if len(filename) == 0 { // no work to do continue } config, err := loadFromFile(filename) if os.IsNotExist(err) { // skip missing files continue } if err != nil { errlist = append(errlist, errors.Wrapf(err, "Error loading config file \"%s\"", filename)) continue } kubeconfigs = append(kubeconfigs, config) } // first merge all of our maps mapConfig := clientcmdNewConfig() for _, kubeconfig := range kubeconfigs { mergo.Merge(mapConfig, kubeconfig) } // merge all of the struct values in the reverse order so that priority is given correctly // errors are not added to the list the second time nonMapConfig := clientcmdNewConfig() for i := len(kubeconfigs) - 1; i >= 0; i-- { kubeconfig := kubeconfigs[i] mergo.Merge(nonMapConfig, kubeconfig) } // since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and // get the values we expect. config := clientcmdNewConfig() mergo.Merge(config, mapConfig) mergo.Merge(config, nonMapConfig) // REMOVED: Possibility to skip this. if err := resolveLocalPaths(config); err != nil { errlist = append(errlist, err) } return config, newAggregate(errlist) } // loadFromFile is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.LoadFromFile // LoadFromFile takes a filename and deserializes the contents into Config object func loadFromFile(filename string) (*clientcmdConfig, error) { kubeconfigBytes, err := ioutil.ReadFile(filename) if err != nil { return nil, err } config, err := load(kubeconfigBytes) if err != nil { return nil, err } // set LocationOfOrigin on every Cluster, User, and Context for key, obj := range config.AuthInfos { obj.LocationOfOrigin = filename config.AuthInfos[key] = obj } for key, obj := range config.Clusters { obj.LocationOfOrigin = filename config.Clusters[key] = obj } for key, obj := range config.Contexts { obj.LocationOfOrigin = filename config.Contexts[key] = obj } if config.AuthInfos == nil { config.AuthInfos = map[string]*clientcmdAuthInfo{} } if config.Clusters == nil { config.Clusters = map[string]*clientcmdCluster{} } if config.Contexts == nil { config.Contexts = map[string]*clientcmdContext{} } return config, nil } // load is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.Load // Load takes a byte slice and deserializes the contents into Config object. // Encapsulates deserialization without assuming the source is a file. func load(data []byte) (*clientcmdConfig, error) { config := clientcmdNewConfig() // if there's no data in a file, return the default object instead of failing (DecodeInto reject empty input) if len(data) == 0 { return config, nil } // Note: This does absolutely no kind/version checking or conversions. data, err := yaml.YAMLToJSON(data) if err != nil { return nil, err } if err := json.Unmarshal(data, config); err != nil { return nil, err } return config, nil } // resolveLocalPaths is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules.resolveLocalPaths. // ResolveLocalPaths resolves all relative paths in the config object with respect to the stanza's LocationOfOrigin // this cannot be done directly inside of LoadFromFile because doing so there would make it impossible to load a file without // modification of its contents. func resolveLocalPaths(config *clientcmdConfig) error { for _, cluster := range config.Clusters { if len(cluster.LocationOfOrigin) == 0 { continue } base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin)) if err != nil { return errors.Wrapf(err, "Could not determine the absolute path of config file %s", cluster.LocationOfOrigin) } if err := resolvePaths(getClusterFileReferences(cluster), base); err != nil { return err } } for _, authInfo := range config.AuthInfos { if len(authInfo.LocationOfOrigin) == 0 { continue } base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin)) if err != nil { return errors.Wrapf(err, "Could not determine the absolute path of config file %s", authInfo.LocationOfOrigin) } if err := resolvePaths(getAuthInfoFileReferences(authInfo), base); err != nil { return err } } return nil } // getClusterFileReferences is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules.GetClusterFileReferences. func getClusterFileReferences(cluster *clientcmdCluster) []*string { return []*string{&cluster.CertificateAuthority} } // getAuthInfoFileReferences is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules.GetAuthInfoFileReferences. func getAuthInfoFileReferences(authInfo *clientcmdAuthInfo) []*string { return []*string{&authInfo.ClientCertificate, &authInfo.ClientKey} } // resolvePaths is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.ClientConfigLoadingRules.resolvePaths. // ResolvePaths updates the given refs to be absolute paths, relative to the given base directory func resolvePaths(refs []*string, base string) error { for _, ref := range refs { // Don't resolve empty paths if len(*ref) > 0 { // Don't resolve absolute paths if !filepath.IsAbs(*ref) { *ref = filepath.Join(base, *ref) } } } return nil } // restClientFor is a modified copy of k8s.io/kubernets/pkg/client/restclient.RESTClientFor. // 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 *restConfig) (*url.URL, *http.Client, error) { // REMOVED: Configurable GroupVersion, Codec // REMOVED: Configurable versionedAPIPath baseURL, err := defaultServerURLFor(config) if err != nil { return nil, nil, err } transport, err := transportFor(config) if err != nil { return nil, nil, err } var httpClient *http.Client if transport != http.DefaultTransport { httpClient = &http.Client{Transport: transport} } // REMOVED: Configurable QPS, Burst, ContentConfig // REMOVED: Actually returning a RESTClient object. return baseURL, httpClient, nil } // defaultServerURL is a modified copy of k8s.io/kubernets/pkg/client/restclient.DefaultServerURL. // 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 string, defaultTLS bool) (*url.URL, error) { if host == "" { return nil, errors.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, errors.Errorf("host must be a URL or a host:port pair: %q", base) } } // REMOVED: versionedAPIPath computation. return hostURL, nil } // defaultServerURLFor is a modified copy of k8s.io/kubernets/pkg/client/restclient.defaultServerURLFor. // defaultServerUrlFor is shared between IsConfigTransportTLS and RESTClientFor. It // requires Host and Version to be set prior to being called. func defaultServerURLFor(config *restConfig) (*url.URL, 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" } // REMOVED: Configurable APIPath, GroupVersion return defaultServerURL(host, defaultTLS) } // transportFor is a modified copy of k8s.io/kubernets/pkg/client/restclient.transportFor. // TransportFor returns an http.RoundTripper that will provide the authentication // or transport level security defined by the provided Config. Will return the // default http.DefaultTransport if no special case behavior is needed. func transportFor(config *restConfig) (http.RoundTripper, error) { // REMOVED: separation between restclient.Config and transport.Config, Transport, WrapTransport support return transportNew(config) } // isConfigTransportTLS is a modified copy of k8s.io/kubernets/pkg/client/restclient.IsConfigTransportTLS. // 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 restConfig) bool { baseURL, err := defaultServerURLFor(&config) if err != nil { return false } return baseURL.Scheme == "https" } // transportNew is a modified copy of k8s.io/kubernetes/pkg/client/transport.New. // New returns an http.RoundTripper that will provide the authentication // or transport level security defined by the provided Config. func transportNew(config *restConfig) (http.RoundTripper, error) { // REMOVED: custom config.Transport support. // Set transport level security var ( rt http.RoundTripper err error ) rt, err = tlsCacheGet(config) if err != nil { return nil, err } // REMOVED: HTTPWrappersForConfig(config, rt) in favor of the caller setting HTTP headers itself based on restConfig. Only this inlined check remains. if len(config.Username) != 0 && len(config.BearerToken) != 0 { return nil, errors.Errorf("username/password or bearer token may be set, but not both") } return rt, nil } // newProxierWithNoProxyCIDR is a modified copy of k8s.io/apimachinery/pkg/util/net.NewProxierWithNoProxyCIDR. // NewProxierWithNoProxyCIDR constructs a Proxier function that respects CIDRs in NO_PROXY and delegates if // no matching CIDRs are found func newProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error)) func(req *http.Request) (*url.URL, error) { // we wrap the default method, so we only need to perform our check if the NO_PROXY envvar has a CIDR in it noProxyEnv := os.Getenv("NO_PROXY") noProxyRules := strings.Split(noProxyEnv, ",") cidrs := []*net.IPNet{} for _, noProxyRule := range noProxyRules { _, cidr, _ := net.ParseCIDR(noProxyRule) if cidr != nil { cidrs = append(cidrs, cidr) } } if len(cidrs) == 0 { return delegate } return func(req *http.Request) (*url.URL, error) { host := req.URL.Host // for some urls, the Host is already the host, not the host:port if net.ParseIP(host) == nil { var err error host, _, err = net.SplitHostPort(req.URL.Host) if err != nil { return delegate(req) } } ip := net.ParseIP(host) if ip == nil { return delegate(req) } for _, cidr := range cidrs { if cidr.Contains(ip) { return nil, nil } } return delegate(req) } } // tlsCacheGet is a modified copy of k8s.io/kubernetes/pkg/client/transport.tlsTransportCache.get. func tlsCacheGet(config *restConfig) (http.RoundTripper, error) { // REMOVED: any actual caching // Get the TLS options for this client config tlsConfig, err := tlsConfigFor(config) if err != nil { return nil, err } // The options didn't require a custom TLS config if tlsConfig == nil { return http.DefaultTransport, nil } // REMOVED: Call to k8s.io/apimachinery/pkg/util/net.SetTransportDefaults; instead of the generic machinery and conditionals, hard-coded the result here. t := &http.Transport{ // http.ProxyFromEnvironment doesn't respect CIDRs and that makes it impossible to exclude things like pod and service IPs from proxy settings // ProxierWithNoProxyCIDR allows CIDR rules in NO_PROXY Proxy: newProxierWithNoProxyCIDR(http.ProxyFromEnvironment), TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: tlsConfig, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).Dial, } // Allow clients to disable http2 if needed. if s := os.Getenv("DISABLE_HTTP2"); len(s) == 0 { _ = http2.ConfigureTransport(t) } return t, nil } // tlsConfigFor is a modified copy of k8s.io/kubernetes/pkg/client/transport.TLSConfigFor. // TLSConfigFor returns a tls.Config that will provide the transport level security defined // by the provided Config. Will return nil if no transport level security is requested. func tlsConfigFor(c *restConfig) (*tls.Config, error) { if !(c.HasCA() || c.HasCertAuth() || c.Insecure) { return nil, nil } if c.HasCA() && c.Insecure { return nil, errors.Errorf("specifying a root certificates file with the insecure flag is not allowed") } if err := loadTLSFiles(c); err != nil { return nil, err } tlsConfig := &tls.Config{ // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability) MinVersion: tls.VersionTLS10, InsecureSkipVerify: c.Insecure, } if c.HasCA() { tlsConfig.RootCAs = rootCertPool(c.CAData) } if c.HasCertAuth() { cert, err := tls.X509KeyPair(c.CertData, c.KeyData) if err != nil { return nil, err } tlsConfig.Certificates = []tls.Certificate{cert} } return tlsConfig, nil } // loadTLSFiles is a modified copy of k8s.io/kubernetes/pkg/client/transport.loadTLSFiles. // 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 *restConfig) 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 is a modified copy of k8s.io/kubernetes/pkg/client/transport.dataFromSliceOrFile. // 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 } // rootCertPool is a modified copy of k8s.io/kubernetes/pkg/client/transport.rootCertPool. // rootCertPool returns nil if caData is empty. When passed along, this will mean "use system CAs". // When caData is not empty, it will be the ONLY information used in the CertPool. func rootCertPool(caData []byte) *x509.CertPool { // What we really want is a copy of x509.systemRootsPool, but that isn't exposed. It's difficult to build (see the go // code for a look at the platform specific insanity), so we'll use the fact that RootCAs == nil gives us the system values // It doesn't allow trusting either/or, but hopefully that won't be an issue if len(caData) == 0 { return nil } // if we have caData, use it certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(caData) return certPool } // HasCA is a modified copy of k8s.io/kubernetes/pkg/client/transport.Config.HasCA. // HasCA returns whether the configuration has a certificate authority or not. func (c *restConfig) HasCA() bool { return len(c.CAData) > 0 || len(c.CAFile) > 0 } // HasCertAuth is a modified copy of k8s.io/kubernetes/pkg/client/transport.Config.HasCertAuth. // HasCertAuth returns whether the configuration has certificate authentication or not. func (c *restConfig) HasCertAuth() bool { return len(c.CertData) != 0 || len(c.CertFile) != 0 } // clientcmdConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.Config. // Config holds the information needed to build connect to remote kubernetes clusters as a given user // IMPORTANT if you add fields to this struct, please update IsConfigEmpty() type clientcmdConfig struct { // Clusters is a map of referencable names to cluster configs Clusters clustersMap `json:"clusters"` // AuthInfos is a map of referencable names to user configs AuthInfos authInfosMap `json:"users"` // Contexts is a map of referencable names to context configs Contexts contextsMap `json:"contexts"` // CurrentContext is the name of the context that you would like to use by default CurrentContext string `json:"current-context"` } type clustersMap map[string]*clientcmdCluster func (m *clustersMap) UnmarshalJSON(data []byte) error { var a []v1NamedCluster if err := json.Unmarshal(data, &a); err != nil { return err } for _, e := range a { cluster := e.Cluster // Allocates a new instance in each iteration (*m)[e.Name] = &cluster } return nil } type authInfosMap map[string]*clientcmdAuthInfo func (m *authInfosMap) UnmarshalJSON(data []byte) error { var a []v1NamedAuthInfo if err := json.Unmarshal(data, &a); err != nil { return err } for _, e := range a { authInfo := e.AuthInfo // Allocates a new instance in each iteration (*m)[e.Name] = &authInfo } return nil } type contextsMap map[string]*clientcmdContext func (m *contextsMap) UnmarshalJSON(data []byte) error { var a []v1NamedContext if err := json.Unmarshal(data, &a); err != nil { return err } for _, e := range a { context := e.Context // Allocates a new instance in each iteration (*m)[e.Name] = &context } return nil } // clientcmdNewConfig is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.NewConfig. // NewConfig is a convenience function that returns a new Config object with non-nil maps func clientcmdNewConfig() *clientcmdConfig { return &clientcmdConfig{ Clusters: make(map[string]*clientcmdCluster), AuthInfos: make(map[string]*clientcmdAuthInfo), Contexts: make(map[string]*clientcmdContext), } } // clientcmdCluster is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.Cluster. // Cluster contains information about how to communicate with a kubernetes cluster type clientcmdCluster struct { // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized. LocationOfOrigin string // Server is the address of the kubernetes cluster (https://hostname:port). Server string `json:"server"` // InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure. InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` // CertificateAuthority is the path to a cert file for the certificate authority. CertificateAuthority string `json:"certificate-authority,omitempty"` // CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` } // clientcmdAuthInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.AuthInfo. // AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are. type clientcmdAuthInfo struct { // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized. LocationOfOrigin string // ClientCertificate is the path to a client cert file for TLS. ClientCertificate string `json:"client-certificate,omitempty"` // ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate ClientCertificateData []byte `json:"client-certificate-data,omitempty"` // ClientKey is the path to a client key file for TLS. ClientKey string `json:"client-key,omitempty"` // ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey ClientKeyData []byte `json:"client-key-data,omitempty"` // Token is the bearer token for authentication to the kubernetes cluster. Token string `json:"token,omitempty"` // Username is the username for basic authentication to the kubernetes cluster. Username string `json:"username,omitempty"` // Password is the password for basic authentication to the kubernetes cluster. Password string `json:"password,omitempty"` } // clientcmdContext is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.Context. // Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) type clientcmdContext struct { // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized. LocationOfOrigin string // Cluster is the name of the cluster for this context Cluster string `json:"cluster"` // AuthInfo is the name of the authInfo for this context AuthInfo string `json:"user"` // Namespace is the default namespace to use on unspecified requests Namespace string `json:"namespace,omitempty"` } // v1NamedCluster is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.v1.NamedCluster. // NamedCluster relates nicknames to cluster information type v1NamedCluster struct { // Name is the nickname for this Cluster Name string `json:"name"` // Cluster holds the cluster information Cluster clientcmdCluster `json:"cluster"` } // v1NamedContext is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.v1.NamedContext. // NamedContext relates nicknames to context information type v1NamedContext struct { // Name is the nickname for this Context Name string `json:"name"` // Context holds the context information Context clientcmdContext `json:"context"` } // v1NamedAuthInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.v1.NamedAuthInfo. // NamedAuthInfo relates nicknames to auth information type v1NamedAuthInfo struct { // Name is the nickname for this AuthInfo Name string `json:"name"` // AuthInfo holds the auth information AuthInfo clientcmdAuthInfo `json:"user"` }