You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

684 lines
19 KiB

/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kube
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/sirupsen/logrus"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
)
var InClusterBaseURL string
func init() {
flag.StringVar(&InClusterBaseURL, "in-cluster-base-url", "https://kubernetes.default", "the base url to request k8s apiserver in cluster")
}
const (
// TestContainerName specifies the primary container name.
TestContainerName = "test"
https = "https"
maxRetries = 8
retryDelay = 2 * time.Second
requestTimeout = time.Minute
// EmptySelector selects everything
EmptySelector = ""
// DefaultClusterAlias specifies the default cluster key to schedule jobs.
DefaultClusterAlias = "default"
)
// newClient is used to allow mocking out the behavior of 'NewClient' while testing.
var newClient = NewClient
// Logger can print debug messages
type Logger interface {
Debugf(s string, v ...interface{})
}
// Client interacts with the Kubernetes api-server.
type Client struct {
// If logger is non-nil, log all method calls with it.
logger Logger
baseURL string
deckURL string
client *http.Client
token string
namespace string
fake bool
hiddenReposProvider func() []string
hiddenOnly bool
}
// SetHiddenReposProvider takes a continuation that fetches a list of orgs and repos for
// which PJs should not be returned.
// NOTE: This function is not thread safe and should be called before the client is in use.
func (c *Client) SetHiddenReposProvider(p func() []string, hiddenOnly bool) {
c.hiddenReposProvider = p
c.hiddenOnly = hiddenOnly
}
// Namespace returns a copy of the client pointing at the specified namespace.
func (c *Client) Namespace(ns string) *Client {
nc := *c
nc.namespace = ns
return &nc
}
func (c *Client) log(methodName string, args ...interface{}) {
if c.logger == nil {
return
}
var as []string
for _, arg := range args {
as = append(as, fmt.Sprintf("%v", arg))
}
c.logger.Debugf("%s(%s)", methodName, strings.Join(as, ", "))
}
// ConflictError is http 409.
type ConflictError struct {
e error
}
func (e ConflictError) Error() string {
return e.e.Error()
}
// NewConflictError returns an error with the embedded inner error
func NewConflictError(e error) ConflictError {
return ConflictError{e: e}
}
// UnprocessableEntityError happens when the apiserver returns http 422.
type UnprocessableEntityError struct {
e error
}
func (e UnprocessableEntityError) Error() string {
return e.e.Error()
}
// NewUnprocessableEntityError returns an error with the embedded inner error
func NewUnprocessableEntityError(e error) UnprocessableEntityError {
return UnprocessableEntityError{e: e}
}
// NotFoundError happens when the apiserver returns http 404
type NotFoundError struct {
e error
}
func (e NotFoundError) Error() string {
return e.e.Error()
}
// NewNotFoundError returns an error with the embedded inner error
func NewNotFoundError(e error) NotFoundError {
return NotFoundError{e: e}
}
type request struct {
method string
path string
deckPath string
query map[string]string
requestBody interface{}
}
func (c *Client) request(r *request, ret interface{}) error {
out, err := c.requestRetry(r)
if err != nil {
return err
}
if ret != nil {
if err := json.Unmarshal(out, ret); err != nil {
return err
}
}
return nil
}
func (c *Client) retry(r *request) (*http.Response, error) {
var resp *http.Response
var err error
backoff := retryDelay
for retries := 0; retries < maxRetries; retries++ {
resp, err = c.doRequest(r.method, r.deckPath, r.path, r.query, r.requestBody)
if err == nil {
if resp.StatusCode < 500 {
break
}
resp.Body.Close()
}
time.Sleep(backoff)
backoff *= 2
}
return resp, err
}
// Retry on transport failures. Does not retry on 500s.
func (c *Client) requestRetryStream(r *request) (io.ReadCloser, error) {
if c.fake && r.deckPath == "" {
return nil, nil
}
resp, err := c.retry(r)
if err != nil {
return nil, err
}
if resp.StatusCode == 409 {
return nil, NewConflictError(fmt.Errorf("body cannot be streamed"))
} else if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, fmt.Errorf("response has status \"%s\"", resp.Status)
}
return resp.Body, nil
}
// Retry on transport failures. Does not retry on 500s.
func (c *Client) requestRetry(r *request) ([]byte, error) {
if c.fake && r.deckPath == "" {
return []byte("{}"), nil
}
resp, err := c.retry(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
rb, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode == 409 {
return nil, NewConflictError(fmt.Errorf("body: %s", string(rb)))
} else if resp.StatusCode == 422 {
return nil, NewUnprocessableEntityError(fmt.Errorf("body: %s", string(rb)))
} else if resp.StatusCode == 404 {
return nil, NewNotFoundError(fmt.Errorf("body: %s", string(rb)))
} else if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, fmt.Errorf("response has status \"%s\" and body \"%s\"", resp.Status, string(rb))
}
return rb, nil
}
func (c *Client) doRequest(method, deckPath, urlPath string, query map[string]string, body interface{}) (*http.Response, error) {
url := c.baseURL + urlPath
if c.deckURL != "" && deckPath != "" {
url = c.deckURL + deckPath
}
var buf io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
buf = bytes.NewBuffer(b)
}
req, err := http.NewRequest(method, url, buf)
if err != nil {
return nil, err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
if method == http.MethodPatch {
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
} else {
req.Header.Set("Content-Type", "application/json")
}
q := req.URL.Query()
for k, v := range query {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
return c.client.Do(req)
}
// NewFakeClient creates a client that doesn't do anything. If you provide a
// deck URL then the client will hit that for the supported calls.
func NewFakeClient(deckURL string) *Client {
return &Client{
namespace: "default",
deckURL: deckURL,
client: &http.Client{},
fake: true,
}
}
// NewClientInCluster creates a Client that works from within a pod.
func NewClientInCluster(namespace string) (*Client, error) {
tokenFile := "/var/run/secrets/kubernetes.io/serviceaccount/token"
token, err := ioutil.ReadFile(tokenFile)
if err != nil {
return nil, err
}
client := &http.Client{Timeout: requestTimeout}
if strings.HasPrefix(InClusterBaseURL, https) {
rootCAFile := "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
certData, err := ioutil.ReadFile(rootCAFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
cp.AppendCertsFromPEM(certData)
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: cp,
},
}
}
return &Client{
logger: logrus.WithField("client", "kube"),
baseURL: InClusterBaseURL,
client: client,
token: string(token),
namespace: namespace,
}, nil
}
// Cluster represents the information necessary to talk to a Kubernetes
// master endpoint.
// NOTE: if your cluster runs on GKE you can use the following command to get these credentials:
// gcloud --project <gcp_project> container clusters describe --zone <zone> <cluster_name>
type Cluster struct {
// The IP address of the cluster's master endpoint.
Endpoint string `json:"endpoint"`
// Base64-encoded public cert used by clients to authenticate to the
// cluster endpoint.
ClientCertificate string `json:"clientCertificate"`
// Base64-encoded private key used by clients..
ClientKey string `json:"clientKey"`
// Base64-encoded public certificate that is the root of trust for the
// cluster.
ClusterCACertificate string `json:"clusterCaCertificate"`
}
// NewClientFromFile reads a Cluster object at clusterPath and returns an
// authenticated client using the keys within.
func NewClientFromFile(clusterPath, namespace string) (*Client, error) {
data, err := ioutil.ReadFile(clusterPath)
if err != nil {
return nil, err
}
var c Cluster
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, err
}
return NewClient(&c, namespace)
}
// UnmarshalClusterMap reads a map[string]Cluster in yaml bytes.
func UnmarshalClusterMap(data []byte) (map[string]Cluster, error) {
var raw map[string]Cluster
if err := yaml.Unmarshal(data, &raw); err != nil {
// If we failed to unmarshal the multicluster format try the single Cluster format.
var singleConfig Cluster
if err := yaml.Unmarshal(data, &singleConfig); err != nil {
return nil, err
}
raw = map[string]Cluster{DefaultClusterAlias: singleConfig}
}
return raw, nil
}
// MarshalClusterMap writes c as yaml bytes.
func MarshalClusterMap(c map[string]Cluster) ([]byte, error) {
return yaml.Marshal(c)
}
// ClientMapFromFile reads the file at clustersPath and attempts to load a map of cluster aliases
// to authenticated clients to the respective clusters.
// The file at clustersPath is expected to be a yaml map from strings to Cluster structs OR it may
// simply be a single Cluster struct which will be assigned the alias $DefaultClusterAlias.
// If the file is an alias map, it must include the alias $DefaultClusterAlias.
func ClientMapFromFile(clustersPath, namespace string) (map[string]*Client, error) {
data, err := ioutil.ReadFile(clustersPath)
if err != nil {
return nil, fmt.Errorf("read error: %v", err)
}
raw, err := UnmarshalClusterMap(data)
if err != nil {
return nil, fmt.Errorf("unmarshal error: %v", err)
}
foundDefault := false
result := map[string]*Client{}
for alias, config := range raw {
client, err := newClient(&config, namespace)
if err != nil {
return nil, fmt.Errorf("failed to load config for build cluster alias %q in file %q: %v", alias, clustersPath, err)
}
result[alias] = client
if alias == DefaultClusterAlias {
foundDefault = true
}
}
if !foundDefault {
return nil, fmt.Errorf("failed to find the required %q alias in build cluster config %q", DefaultClusterAlias, clustersPath)
}
return result, nil
}
// NewClient returns an authenticated Client using the keys in the Cluster.
func NewClient(c *Cluster, namespace string) (*Client, error) {
cc, err := base64.StdEncoding.DecodeString(c.ClientCertificate)
if err != nil {
return nil, err
}
ck, err := base64.StdEncoding.DecodeString(c.ClientKey)
if err != nil {
return nil, err
}
ca, err := base64.StdEncoding.DecodeString(c.ClusterCACertificate)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(cc, ck)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
cp.AppendCertsFromPEM(ca)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
RootCAs: cp,
},
}
return &Client{
logger: logrus.WithField("client", "kube"),
baseURL: c.Endpoint,
client: &http.Client{Transport: tr, Timeout: requestTimeout},
namespace: namespace,
}, nil
}
// GetPod is analogous to kubectl get pods/NAME namespace=client.namespace
func (c *Client) GetPod(name string) (Pod, error) {
c.log("GetPod", name)
var retPod Pod
err := c.request(&request{
path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", c.namespace, name),
}, &retPod)
return retPod, err
}
// ListPods is analogous to kubectl get pods --selector=SELECTOR --namespace=client.namespace
func (c *Client) ListPods(selector string) ([]Pod, error) {
c.log("ListPods", selector)
var pl struct {
Items []Pod `json:"items"`
}
err := c.request(&request{
path: fmt.Sprintf("/api/v1/namespaces/%s/pods", c.namespace),
query: map[string]string{"labelSelector": selector},
}, &pl)
return pl.Items, err
}
// DeletePod deletes the pod at name in the client's default namespace.
//
// Analogous to kubectl delete pod
func (c *Client) DeletePod(name string) error {
c.log("DeletePod", name)
return c.request(&request{
method: http.MethodDelete,
path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", c.namespace, name),
}, nil)
}
// CreateProwJob creates a prowjob in the client's default namespace.
//
// Analogous to kubectl create prowjob
func (c *Client) CreateProwJob(j ProwJob) (ProwJob, error) {
var representation string
if out, err := json.Marshal(j); err == nil {
representation = string(out[:])
} else {
representation = fmt.Sprintf("%v", j)
}
c.log("CreateProwJob", representation)
var retJob ProwJob
err := c.request(&request{
method: http.MethodPost,
path: fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs", c.namespace),
requestBody: &j,
}, &retJob)
return retJob, err
}
func (c *Client) getHiddenRepos() sets.String {
if c.hiddenReposProvider == nil {
return nil
}
return sets.NewString(c.hiddenReposProvider()...)
}
func shouldHide(pj *ProwJob, hiddenRepos sets.String, showHiddenOnly bool) bool {
if pj.Spec.Refs == nil {
// periodic jobs do not have refs and therefore cannot be
// hidden by the org/repo mechanism
return false
}
shouldHide := hiddenRepos.HasAny(fmt.Sprintf("%s/%s", pj.Spec.Refs.Org, pj.Spec.Refs.Repo), pj.Spec.Refs.Org)
if showHiddenOnly {
return !shouldHide
}
return shouldHide
}
// GetProwJob returns the prowjob at name in the client's default namespace.
//
// Analogous to kubectl get prowjob/NAME
func (c *Client) GetProwJob(name string) (ProwJob, error) {
c.log("GetProwJob", name)
var pj ProwJob
err := c.request(&request{
path: fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs/%s", c.namespace, name),
}, &pj)
if err == nil && shouldHide(&pj, c.getHiddenRepos(), c.hiddenOnly) {
pj = ProwJob{}
// Revealing the existence of this prow job is ok because the pj name cannot be used to
// retrieve the pj itself. Furthermore, a timing attack could differentiate true 404s from
// 404s returned when a hidden pj is queried so returning a 404 wouldn't hide the pj's existence.
err = errors.New("403 ProwJob is hidden")
}
return pj, err
}
// ListProwJobs lists prowjobs using the specified labelSelector in the client's default namespace.
//
// Analogous to kubectl get prowjobs --selector=SELECTOR
func (c *Client) ListProwJobs(selector string) ([]ProwJob, error) {
c.log("ListProwJobs", selector)
var jl struct {
Items []ProwJob `json:"items"`
}
err := c.request(&request{
path: fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs", c.namespace),
deckPath: "/prowjobs.js",
query: map[string]string{"labelSelector": selector},
}, &jl)
if err == nil {
hidden := c.getHiddenRepos()
var pjs []ProwJob
for _, pj := range jl.Items {
if !shouldHide(&pj, hidden, c.hiddenOnly) {
pjs = append(pjs, pj)
}
}
jl.Items = pjs
}
return jl.Items, err
}
// DeleteProwJob deletes the prowjob at name in the client's default namespace.
func (c *Client) DeleteProwJob(name string) error {
c.log("DeleteProwJob", name)
return c.request(&request{
method: http.MethodDelete,
path: fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs/%s", c.namespace, name),
}, nil)
}
// ReplaceProwJob will replace name with job in the client's default namespace.
//
// Analogous to kubectl replace prowjobs/NAME
func (c *Client) ReplaceProwJob(name string, job ProwJob) (ProwJob, error) {
c.log("ReplaceProwJob", name, job)
var retJob ProwJob
err := c.request(&request{
method: http.MethodPut,
path: fmt.Sprintf("/apis/prow.k8s.io/v1/namespaces/%s/prowjobs/%s", c.namespace, name),
requestBody: &job,
}, &retJob)
return retJob, err
}
// CreatePod creates a pod in the client's default namespace.
//
// Analogous to kubectl create pod
func (c *Client) CreatePod(p v1.Pod) (Pod, error) {
c.log("CreatePod", p)
var retPod Pod
err := c.request(&request{
method: http.MethodPost,
path: fmt.Sprintf("/api/v1/namespaces/%s/pods", c.namespace),
requestBody: &p,
}, &retPod)
return retPod, err
}
// GetLog returns the log of the default container in the specified pod, in the client's default namespace.
//
// Analogous to kubectl logs pod
func (c *Client) GetLog(pod string) ([]byte, error) {
c.log("GetLog", pod)
return c.requestRetry(&request{
path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", c.namespace, pod),
})
}
// GetLogTail returns the last n bytes of the log of the specified container in the specified pod,
// in the client's default namespace.
//
// Analogous to kubectl logs pod --tail -1 --limit-bytes n -c container
func (c *Client) GetLogTail(pod, container string, n int64) ([]byte, error) {
c.log("GetLogTail", pod, n)
return c.requestRetry(&request{
path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", c.namespace, pod),
query: map[string]string{ // Because we want last n bytes, we fetch all lines and then limit to n bytes
"tailLines": "-1",
"container": container,
"limitBytes": strconv.FormatInt(n, 10),
},
})
}
// GetContainerLog returns the log of a container in the specified pod, in the client's default namespace.
//
// Analogous to kubectl logs pod -c container
func (c *Client) GetContainerLog(pod, container string) ([]byte, error) {
c.log("GetContainerLog", pod)
return c.requestRetry(&request{
path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", c.namespace, pod),
query: map[string]string{"container": container},
})
}
// CreateConfigMap creates a configmap.
//
// Analogous to kubectl create configmap
func (c *Client) CreateConfigMap(content ConfigMap) (ConfigMap, error) {
c.log("CreateConfigMap")
var retConfigMap ConfigMap
err := c.request(&request{
method: http.MethodPost,
path: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", c.namespace),
requestBody: &content,
}, &retConfigMap)
return retConfigMap, err
}
// GetConfigMap gets the configmap identified.
func (c *Client) GetConfigMap(name, namespace string) (ConfigMap, error) {
c.log("GetConfigMap", name)
if namespace == "" {
namespace = c.namespace
}
var retConfigMap ConfigMap
err := c.request(&request{
path: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", namespace, name),
}, &retConfigMap)
return retConfigMap, err
}
// ReplaceConfigMap puts the configmap into name.
//
// Analogous to kubectl replace configmap
//
// If config.Namespace is empty, the client's default namespace is used.
// Returns the content returned by the apiserver
func (c *Client) ReplaceConfigMap(name string, config ConfigMap) (ConfigMap, error) {
c.log("ReplaceConfigMap", name)
namespace := c.namespace
if config.Namespace != "" {
namespace = config.Namespace
}
var retConfigMap ConfigMap
err := c.request(&request{
method: http.MethodPut,
path: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", namespace, name),
requestBody: &config,
}, &retConfigMap)
return retConfigMap, err
}