github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/kube/client.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kube
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/base64"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/ghodss/yaml"
    34  	"github.com/sirupsen/logrus"
    35  	"k8s.io/api/core/v1"
    36  	"k8s.io/apimachinery/pkg/util/sets"
    37  )
    38  
    39  const (
    40  	// TestContainerName specifies the primary container name.
    41  	TestContainerName = "test"
    42  
    43  	inClusterBaseURL = "https://kubernetes.default"
    44  	maxRetries       = 8
    45  	retryDelay       = 2 * time.Second
    46  	requestTimeout   = time.Minute
    47  
    48  	// EmptySelector selects everything
    49  	EmptySelector = ""
    50  
    51  	// DefaultClusterAlias specifies the default cluster key to schedule jobs.
    52  	DefaultClusterAlias = "default"
    53  )
    54  
    55  // newClient is used to allow mocking out the behavior of 'NewClient' while testing.
    56  var newClient = NewClient
    57  
    58  // Logger can print debug messages
    59  type Logger interface {
    60  	Debugf(s string, v ...interface{})
    61  }
    62  
    63  // Client interacts with the Kubernetes api-server.
    64  type Client struct {
    65  	// If logger is non-nil, log all method calls with it.
    66  	logger Logger
    67  
    68  	baseURL   string
    69  	deckURL   string
    70  	client    *http.Client
    71  	token     string
    72  	namespace string
    73  	fake      bool
    74  
    75  	hiddenReposProvider func() []string
    76  	hiddenOnly          bool
    77  }
    78  
    79  // SetHiddenReposProvider takes a continuation that fetches a list of orgs and repos for
    80  // which PJs should not be returned.
    81  // NOTE: This function is not thread safe and should be called before the client is in use.
    82  func (c *Client) SetHiddenReposProvider(p func() []string, hiddenOnly bool) {
    83  	c.hiddenReposProvider = p
    84  	c.hiddenOnly = hiddenOnly
    85  }
    86  
    87  // Namespace returns a copy of the client pointing at the specified namespace.
    88  func (c *Client) Namespace(ns string) *Client {
    89  	nc := *c
    90  	nc.namespace = ns
    91  	return &nc
    92  }
    93  
    94  func (c *Client) log(methodName string, args ...interface{}) {
    95  	if c.logger == nil {
    96  		return
    97  	}
    98  	var as []string
    99  	for _, arg := range args {
   100  		as = append(as, fmt.Sprintf("%v", arg))
   101  	}
   102  	c.logger.Debugf("%s(%s)", methodName, strings.Join(as, ", "))
   103  }
   104  
   105  // ConflictError is http 409.
   106  type ConflictError struct {
   107  	e error
   108  }
   109  
   110  func (e ConflictError) Error() string {
   111  	return e.e.Error()
   112  }
   113  
   114  // NewConflictError returns an error with the embedded inner error
   115  func NewConflictError(e error) ConflictError {
   116  	return ConflictError{e: e}
   117  }
   118  
   119  // UnprocessableEntityError happens when the apiserver returns http 422.
   120  type UnprocessableEntityError struct {
   121  	e error
   122  }
   123  
   124  func (e UnprocessableEntityError) Error() string {
   125  	return e.e.Error()
   126  }
   127  
   128  // NewUnprocessableEntityError returns an error with the embedded inner error
   129  func NewUnprocessableEntityError(e error) UnprocessableEntityError {
   130  	return UnprocessableEntityError{e: e}
   131  }
   132  
   133  // NotFoundError happens when the apiserver returns http 404
   134  type NotFoundError struct {
   135  	e error
   136  }
   137  
   138  func (e NotFoundError) Error() string {
   139  	return e.e.Error()
   140  }
   141  
   142  // NewNotFoundError returns an error with the embedded inner error
   143  func NewNotFoundError(e error) NotFoundError {
   144  	return NotFoundError{e: e}
   145  }
   146  
   147  type request struct {
   148  	method      string
   149  	path        string
   150  	deckPath    string
   151  	query       map[string]string
   152  	requestBody interface{}
   153  }
   154  
   155  func (c *Client) request(r *request, ret interface{}) error {
   156  	out, err := c.requestRetry(r)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	if ret != nil {
   161  		if err := json.Unmarshal(out, ret); err != nil {
   162  			return err
   163  		}
   164  	}
   165  	return nil
   166  }
   167  
   168  func (c *Client) retry(r *request) (*http.Response, error) {
   169  	var resp *http.Response
   170  	var err error
   171  	backoff := retryDelay
   172  	for retries := 0; retries < maxRetries; retries++ {
   173  		resp, err = c.doRequest(r.method, r.deckPath, r.path, r.query, r.requestBody)
   174  		if err == nil {
   175  			if resp.StatusCode < 500 {
   176  				break
   177  			}
   178  			resp.Body.Close()
   179  		}
   180  
   181  		time.Sleep(backoff)
   182  		backoff *= 2
   183  	}
   184  	return resp, err
   185  }
   186  
   187  // Retry on transport failures. Does not retry on 500s.
   188  func (c *Client) requestRetryStream(r *request) (io.ReadCloser, error) {
   189  	if c.fake && r.deckPath == "" {
   190  		return nil, nil
   191  	}
   192  	resp, err := c.retry(r)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	if resp.StatusCode == 409 {
   197  		return nil, NewConflictError(fmt.Errorf("body cannot be streamed"))
   198  	} else if resp.StatusCode < 200 || resp.StatusCode > 299 {
   199  		return nil, fmt.Errorf("response has status \"%s\"", resp.Status)
   200  	}
   201  	return resp.Body, nil
   202  }
   203  
   204  // Retry on transport failures. Does not retry on 500s.
   205  func (c *Client) requestRetry(r *request) ([]byte, error) {
   206  	if c.fake && r.deckPath == "" {
   207  		return []byte("{}"), nil
   208  	}
   209  	resp, err := c.retry(r)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	defer resp.Body.Close()
   215  	rb, err := ioutil.ReadAll(resp.Body)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	if resp.StatusCode == 409 {
   220  		return nil, NewConflictError(fmt.Errorf("body: %s", string(rb)))
   221  	} else if resp.StatusCode == 422 {
   222  		return nil, NewUnprocessableEntityError(fmt.Errorf("body: %s", string(rb)))
   223  	} else if resp.StatusCode == 404 {
   224  		return nil, NewNotFoundError(fmt.Errorf("body: %s", string(rb)))
   225  	} else if resp.StatusCode < 200 || resp.StatusCode > 299 {
   226  		return nil, fmt.Errorf("response has status \"%s\" and body \"%s\"", resp.Status, string(rb))
   227  	}
   228  	return rb, nil
   229  }
   230  
   231  func (c *Client) doRequest(method, deckPath, urlPath string, query map[string]string, body interface{}) (*http.Response, error) {
   232  	url := c.baseURL + urlPath
   233  	if c.deckURL != "" && deckPath != "" {
   234  		url = c.deckURL + deckPath
   235  	}
   236  	var buf io.Reader
   237  	if body != nil {
   238  		b, err := json.Marshal(body)
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		buf = bytes.NewBuffer(b)
   243  	}
   244  	req, err := http.NewRequest(method, url, buf)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  	if c.token != "" {
   249  		req.Header.Set("Authorization", "Bearer "+c.token)
   250  	}
   251  	if method == http.MethodPatch {
   252  		req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
   253  	} else {
   254  		req.Header.Set("Content-Type", "application/json")
   255  	}
   256  
   257  	q := req.URL.Query()
   258  	for k, v := range query {
   259  		q.Add(k, v)
   260  	}
   261  	req.URL.RawQuery = q.Encode()
   262  
   263  	return c.client.Do(req)
   264  }
   265  
   266  // NewFakeClient creates a client that doesn't do anything. If you provide a
   267  // deck URL then the client will hit that for the supported calls.
   268  func NewFakeClient(deckURL string) *Client {
   269  	return &Client{
   270  		namespace: "default",
   271  		deckURL:   deckURL,
   272  		client:    &http.Client{},
   273  		fake:      true,
   274  	}
   275  }
   276  
   277  // NewClientInCluster creates a Client that works from within a pod.
   278  func NewClientInCluster(namespace string) (*Client, error) {
   279  	tokenFile := "/var/run/secrets/kubernetes.io/serviceaccount/token"
   280  	token, err := ioutil.ReadFile(tokenFile)
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	rootCAFile := "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
   286  	certData, err := ioutil.ReadFile(rootCAFile)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	cp := x509.NewCertPool()
   292  	cp.AppendCertsFromPEM(certData)
   293  
   294  	tr := &http.Transport{
   295  		TLSClientConfig: &tls.Config{
   296  			MinVersion: tls.VersionTLS12,
   297  			RootCAs:    cp,
   298  		},
   299  	}
   300  	return &Client{
   301  		logger:    logrus.WithField("client", "kube"),
   302  		baseURL:   inClusterBaseURL,
   303  		client:    &http.Client{Transport: tr, Timeout: requestTimeout},
   304  		token:     string(token),
   305  		namespace: namespace,
   306  	}, nil
   307  }
   308  
   309  // Cluster represents the information necessary to talk to a Kubernetes
   310  // master endpoint.
   311  // NOTE: if your cluster runs on GKE you can use the following command to get these credentials:
   312  // gcloud --project <gcp_project> container clusters describe --zone <zone> <cluster_name>
   313  type Cluster struct {
   314  	// The IP address of the cluster's master endpoint.
   315  	Endpoint string `json:"endpoint"`
   316  	// Base64-encoded public cert used by clients to authenticate to the
   317  	// cluster endpoint.
   318  	ClientCertificate string `json:"clientCertificate"`
   319  	// Base64-encoded private key used by clients..
   320  	ClientKey string `json:"clientKey"`
   321  	// Base64-encoded public certificate that is the root of trust for the
   322  	// cluster.
   323  	ClusterCACertificate string `json:"clusterCaCertificate"`
   324  }
   325  
   326  // NewClientFromFile reads a Cluster object at clusterPath and returns an
   327  // authenticated client using the keys within.
   328  func NewClientFromFile(clusterPath, namespace string) (*Client, error) {
   329  	data, err := ioutil.ReadFile(clusterPath)
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  	var c Cluster
   334  	if err := yaml.Unmarshal(data, &c); err != nil {
   335  		return nil, err
   336  	}
   337  	return NewClient(&c, namespace)
   338  }
   339  
   340  // UnmarshalClusterMap reads a map[string]Cluster in yaml bytes.
   341  func UnmarshalClusterMap(data []byte) (map[string]Cluster, error) {
   342  	var raw map[string]Cluster
   343  	if err := yaml.Unmarshal(data, &raw); err != nil {
   344  		// If we failed to unmarshal the multicluster format try the single Cluster format.
   345  		var singleConfig Cluster
   346  		if err := yaml.Unmarshal(data, &singleConfig); err != nil {
   347  			return nil, err
   348  		}
   349  		raw = map[string]Cluster{DefaultClusterAlias: singleConfig}
   350  	}
   351  	return raw, nil
   352  }
   353  
   354  // MarshalClusterMap writes c as yaml bytes.
   355  func MarshalClusterMap(c map[string]Cluster) ([]byte, error) {
   356  	return yaml.Marshal(c)
   357  }
   358  
   359  // ClientMapFromFile reads the file at clustersPath and attempts to load a map of cluster aliases
   360  // to authenticated clients to the respective clusters.
   361  // The file at clustersPath is expected to be a yaml map from strings to Cluster structs OR it may
   362  // simply be a single Cluster struct which will be assigned the alias $DefaultClusterAlias.
   363  // If the file is an alias map, it must include the alias $DefaultClusterAlias.
   364  func ClientMapFromFile(clustersPath, namespace string) (map[string]*Client, error) {
   365  	data, err := ioutil.ReadFile(clustersPath)
   366  	if err != nil {
   367  		return nil, fmt.Errorf("read error: %v", err)
   368  	}
   369  	raw, err := UnmarshalClusterMap(data)
   370  	if err != nil {
   371  		return nil, fmt.Errorf("unmarshal error: %v", err)
   372  	}
   373  	foundDefault := false
   374  	result := map[string]*Client{}
   375  	for alias, config := range raw {
   376  		client, err := newClient(&config, namespace)
   377  		if err != nil {
   378  			return nil, fmt.Errorf("failed to load config for build cluster alias %q in file %q: %v", alias, clustersPath, err)
   379  		}
   380  		result[alias] = client
   381  		if alias == DefaultClusterAlias {
   382  			foundDefault = true
   383  		}
   384  	}
   385  	if !foundDefault {
   386  		return nil, fmt.Errorf("failed to find the required %q alias in build cluster config %q", DefaultClusterAlias, clustersPath)
   387  	}
   388  	return result, nil
   389  }
   390  
   391  // NewClient returns an authenticated Client using the keys in the Cluster.
   392  func NewClient(c *Cluster, namespace string) (*Client, error) {
   393  	cc, err := base64.StdEncoding.DecodeString(c.ClientCertificate)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	ck, err := base64.StdEncoding.DecodeString(c.ClientKey)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  	ca, err := base64.StdEncoding.DecodeString(c.ClusterCACertificate)
   402  	if err != nil {
   403  		return nil, err
   404  	}
   405  
   406  	cert, err := tls.X509KeyPair(cc, ck)
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	cp := x509.NewCertPool()
   412  	cp.AppendCertsFromPEM(ca)
   413  
   414  	tr := &http.Transport{
   415  		TLSClientConfig: &tls.Config{
   416  			MinVersion:   tls.VersionTLS12,
   417  			Certificates: []tls.Certificate{cert},
   418  			RootCAs:      cp,
   419  		},
   420  	}
   421  	return &Client{
   422  		logger:    logrus.WithField("client", "kube"),
   423  		baseURL:   c.Endpoint,
   424  		client:    &http.Client{Transport: tr, Timeout: requestTimeout},
   425  		namespace: namespace,
   426  	}, nil
   427  }
   428  
   429  // GetPod is analogous to kubectl get pods/NAME namespace=client.namespace
   430  func (c *Client) GetPod(name string) (Pod, error) {
   431  	c.log("GetPod", name)
   432  	var retPod Pod
   433  	err := c.request(&request{
   434  		path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", c.namespace, name),
   435  	}, &retPod)
   436  	return retPod, err
   437  }
   438  
   439  // ListPods is analogous to kubectl get pods --selector=SELECTOR --namespace=client.namespace
   440  func (c *Client) ListPods(selector string) ([]Pod, error) {
   441  	c.log("ListPods", selector)
   442  	var pl struct {
   443  		Items []Pod `json:"items"`
   444  	}
   445  	err := c.request(&request{
   446  		path:  fmt.Sprintf("/api/v1/namespaces/%s/pods", c.namespace),
   447  		query: map[string]string{"labelSelector": selector},
   448  	}, &pl)
   449  	return pl.Items, err
   450  }
   451  
   452  // DeletePod deletes the pod at name in the client's default namespace.
   453  //
   454  // Analogous to kubectl delete pod
   455  func (c *Client) DeletePod(name string) error {
   456  	c.log("DeletePod", name)
   457  	return c.request(&request{
   458  		method: http.MethodDelete,
   459  		path:   fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", c.namespace, name),
   460  	}, nil)
   461  }
   462  
   463  // CreateProwJob creates a prowjob in the client's default namespace.
   464  //
   465  // Analogous to kubectl create prowjob
   466  func (c *Client) CreateProwJob(j ProwJob) (ProwJob, error) {
   467  	var representation string
   468  	if out, err := json.Marshal(j); err == nil {
   469  		representation = string(out[:])
   470  	} else {
   471  		representation = fmt.Sprintf("%v", j)
   472  	}
   473  	c.log("CreateProwJob", representation)
   474  	var retJob ProwJob
   475  	err := c.request(&request{
   476  		method:      http.MethodPost,
   477  		path:        fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs", c.namespace),
   478  		requestBody: &j,
   479  	}, &retJob)
   480  	return retJob, err
   481  }
   482  
   483  func (c *Client) getHiddenRepos() sets.String {
   484  	if c.hiddenReposProvider == nil {
   485  		return nil
   486  	}
   487  	return sets.NewString(c.hiddenReposProvider()...)
   488  }
   489  
   490  func shouldHide(pj *ProwJob, hiddenRepos sets.String, showHiddenOnly bool) bool {
   491  	if pj.Spec.Refs == nil {
   492  		// periodic jobs do not have refs and therefore cannot be
   493  		// hidden by the org/repo mechanism
   494  		return false
   495  	}
   496  	shouldHide := hiddenRepos.HasAny(fmt.Sprintf("%s/%s", pj.Spec.Refs.Org, pj.Spec.Refs.Repo), pj.Spec.Refs.Org)
   497  	if showHiddenOnly {
   498  		return !shouldHide
   499  	}
   500  	return shouldHide
   501  }
   502  
   503  // GetProwJob returns the prowjob at name in the client's default namespace.
   504  //
   505  // Analogous to kubectl get prowjob/NAME
   506  func (c *Client) GetProwJob(name string) (ProwJob, error) {
   507  	c.log("GetProwJob", name)
   508  	var pj ProwJob
   509  	err := c.request(&request{
   510  		path: fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs/%s", c.namespace, name),
   511  	}, &pj)
   512  	if err == nil && shouldHide(&pj, c.getHiddenRepos(), c.hiddenOnly) {
   513  		pj = ProwJob{}
   514  		// Revealing the existence of this prow job is ok because the pj name cannot be used to
   515  		// retrieve the pj itself. Furthermore, a timing attack could differentiate true 404s from
   516  		// 404s returned when a hidden pj is queried so returning a 404 wouldn't hide the pj's existence.
   517  		err = errors.New("403 ProwJob is hidden")
   518  	}
   519  	return pj, err
   520  }
   521  
   522  // ListProwJobs lists prowjobs using the specified labelSelector in the client's default namespace.
   523  //
   524  // Analogous to kubectl get prowjobs --selector=SELECTOR
   525  func (c *Client) ListProwJobs(selector string) ([]ProwJob, error) {
   526  	c.log("ListProwJobs", selector)
   527  	var jl struct {
   528  		Items []ProwJob `json:"items"`
   529  	}
   530  	err := c.request(&request{
   531  		path:     fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs", c.namespace),
   532  		deckPath: "/prowjobs.js",
   533  		query:    map[string]string{"labelSelector": selector},
   534  	}, &jl)
   535  	if err == nil {
   536  		hidden := c.getHiddenRepos()
   537  		var pjs []ProwJob
   538  		for _, pj := range jl.Items {
   539  			if !shouldHide(&pj, hidden, c.hiddenOnly) {
   540  				pjs = append(pjs, pj)
   541  			}
   542  		}
   543  		jl.Items = pjs
   544  	}
   545  	return jl.Items, err
   546  }
   547  
   548  // DeleteProwJob deletes the prowjob at name in the client's default namespace.
   549  func (c *Client) DeleteProwJob(name string) error {
   550  	c.log("DeleteProwJob", name)
   551  	return c.request(&request{
   552  		method: http.MethodDelete,
   553  		path:   fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs/%s", c.namespace, name),
   554  	}, nil)
   555  }
   556  
   557  // ReplaceProwJob will replace name with job in the client's default namespace.
   558  //
   559  // Analogous to kubectl replace prowjobs/NAME
   560  func (c *Client) ReplaceProwJob(name string, job ProwJob) (ProwJob, error) {
   561  	c.log("ReplaceProwJob", name, job)
   562  	var retJob ProwJob
   563  	err := c.request(&request{
   564  		method:      http.MethodPut,
   565  		path:        fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs/%s", c.namespace, name),
   566  		requestBody: &job,
   567  	}, &retJob)
   568  	return retJob, err
   569  }
   570  
   571  // CreatePod creates a pod in the client's default namespace.
   572  //
   573  // Analogous to kubectl create pod
   574  func (c *Client) CreatePod(p v1.Pod) (Pod, error) {
   575  	c.log("CreatePod", p)
   576  	var retPod Pod
   577  	err := c.request(&request{
   578  		method:      http.MethodPost,
   579  		path:        fmt.Sprintf("/api/v1/namespaces/%s/pods", c.namespace),
   580  		requestBody: &p,
   581  	}, &retPod)
   582  	return retPod, err
   583  }
   584  
   585  // GetLog returns the log of the default container in the specified pod, in the client's default namespace.
   586  //
   587  // Analogous to kubectl logs pod
   588  func (c *Client) GetLog(pod string) ([]byte, error) {
   589  	c.log("GetLog", pod)
   590  	return c.requestRetry(&request{
   591  		path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", c.namespace, pod),
   592  	})
   593  }
   594  
   595  // GetContainerLog returns the log of a container in the specified pod, in the client's default namespace.
   596  //
   597  // Analogous to kubectl logs pod -c container
   598  func (c *Client) GetContainerLog(pod, container string) ([]byte, error) {
   599  	c.log("GetContainerLog", pod)
   600  	return c.requestRetry(&request{
   601  		path:  fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", c.namespace, pod),
   602  		query: map[string]string{"container": container},
   603  	})
   604  }
   605  
   606  // CreateConfigMap creates a configmap.
   607  //
   608  // Analogous to kubectl create configmap
   609  func (c *Client) CreateConfigMap(content ConfigMap) (ConfigMap, error) {
   610  	c.log("CreateConfigMap")
   611  	var retConfigMap ConfigMap
   612  	err := c.request(&request{
   613  		method:      http.MethodPost,
   614  		path:        fmt.Sprintf("/api/v1/namespaces/%s/configmaps", c.namespace),
   615  		requestBody: &content,
   616  	}, &retConfigMap)
   617  
   618  	return retConfigMap, err
   619  }
   620  
   621  // GetConfigMap gets the configmap identified.
   622  func (c *Client) GetConfigMap(name, namespace string) (ConfigMap, error) {
   623  	c.log("GetConfigMap", name)
   624  	if namespace == "" {
   625  		namespace = c.namespace
   626  	}
   627  	var retConfigMap ConfigMap
   628  	err := c.request(&request{
   629  		path: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", namespace, name),
   630  	}, &retConfigMap)
   631  
   632  	return retConfigMap, err
   633  }
   634  
   635  // ReplaceConfigMap puts the configmap into name.
   636  //
   637  // Analogous to kubectl replace configmap
   638  //
   639  // If config.Namespace is empty, the client's default namespace is used.
   640  // Returns the content returned by the apiserver
   641  func (c *Client) ReplaceConfigMap(name string, config ConfigMap) (ConfigMap, error) {
   642  	c.log("ReplaceConfigMap", name)
   643  	namespace := c.namespace
   644  	if config.Namespace != "" {
   645  		namespace = config.Namespace
   646  	}
   647  	var retConfigMap ConfigMap
   648  	err := c.request(&request{
   649  		method:      http.MethodPut,
   650  		path:        fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", namespace, name),
   651  		requestBody: &config,
   652  	}, &retConfigMap)
   653  
   654  	return retConfigMap, err
   655  }