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 }