| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package gke contains code for interacting with Google Container Engine (GKE), |
| // the hosted version of Kubernetes on Google Cloud Platform. |
| // |
| // The API is not subject to the Go 1 compatibility promise and may change at |
| // any time. Users should vendor this package and deal with API changes. |
| package gke |
| |
| import ( |
| "context" |
| "crypto/tls" |
| "crypto/x509" |
| "encoding/base64" |
| "fmt" |
| "net/http" |
| |
| "cloud.google.com/go/compute/metadata" |
| |
| "golang.org/x/build/kubernetes" |
| "golang.org/x/oauth2" |
| "golang.org/x/oauth2/google" |
| compute "google.golang.org/api/compute/v1" |
| "google.golang.org/api/container/v1" |
| ) |
| |
| // ClientOpt represents an option that can be passed to the Client function. |
| type ClientOpt interface { |
| modify(*clientOpt) |
| } |
| |
| type clientOpt struct { |
| Project string |
| TokenSource oauth2.TokenSource |
| Zone string |
| Namespace string |
| } |
| |
| type clientOptFunc func(*clientOpt) |
| |
| func (f clientOptFunc) modify(o *clientOpt) { f(o) } |
| |
| // OptProject returns an option setting the GCE Project ID to projectName. |
| // This is the named project ID, not the numeric ID. |
| // If unspecified, the current active project ID is used, if the program is running |
| // on a GCE intance. |
| func OptProject(projectName string) ClientOpt { |
| return clientOptFunc(func(o *clientOpt) { |
| o.Project = projectName |
| }) |
| } |
| |
| // OptZone specifies the GCP zone the cluster is located in. |
| // This is necessary if and only if there are multiple GKE clusters with |
| // the same name in different zones. |
| func OptZone(zoneName string) ClientOpt { |
| return clientOptFunc(func(o *clientOpt) { |
| o.Zone = zoneName |
| }) |
| } |
| |
| // OptTokenSource sets the oauth2 token source for making |
| // authenticated requests to the GKE API. If unset, the default token |
| // source is used (https://godoc.org/golang.org/x/oauth2/google#DefaultTokenSource). |
| func OptTokenSource(ts oauth2.TokenSource) ClientOpt { |
| return clientOptFunc(func(o *clientOpt) { |
| o.TokenSource = ts |
| }) |
| } |
| |
| // OptNamespace sets the Kubernetes namespace to look in. |
| func OptNamespace(namespace string) ClientOpt { |
| return clientOptFunc(func(o *clientOpt) { |
| o.Namespace = namespace |
| }) |
| } |
| |
| // NewClient returns an Kubernetes client to a GKE cluster. |
| func NewClient(ctx context.Context, clusterName string, opts ...ClientOpt) (*kubernetes.Client, error) { |
| opt := clientOpt{Namespace: "default"} |
| for _, o := range opts { |
| o.modify(&opt) |
| } |
| if opt.TokenSource == nil { |
| var err error |
| opt.TokenSource, err = google.DefaultTokenSource(ctx, compute.CloudPlatformScope) |
| if err != nil { |
| return nil, fmt.Errorf("failed to get a token source: %v", err) |
| } |
| } |
| if opt.Project == "" { |
| proj, err := metadata.ProjectID() |
| if err != nil { |
| return nil, fmt.Errorf("metadata.ProjectID: %v", err) |
| } |
| opt.Project = proj |
| } |
| |
| httpClient := oauth2.NewClient(ctx, opt.TokenSource) |
| containerService, err := container.New(httpClient) |
| if err != nil { |
| return nil, fmt.Errorf("could not create client for Google Container Engine: %v", err) |
| } |
| |
| var cluster *container.Cluster |
| if opt.Zone == "" { |
| clusters, err := containerService.Projects.Zones.Clusters.List(opt.Project, "-").Context(ctx).Do() |
| if err != nil { |
| return nil, err |
| } |
| if len(clusters.MissingZones) > 0 { |
| return nil, fmt.Errorf("GKE cluster list response contains missing zones: %v", clusters.MissingZones) |
| } |
| matches := 0 |
| for _, cl := range clusters.Clusters { |
| if cl.Name == clusterName { |
| cluster = cl |
| matches++ |
| } |
| } |
| if matches == 0 { |
| return nil, fmt.Errorf("cluster %q not found in any zone", clusterName) |
| } |
| if matches > 1 { |
| return nil, fmt.Errorf("cluster %q is ambiguous without using gke.OptZone to specify a zone", clusterName) |
| } |
| } else { |
| cluster, err = containerService.Projects.Zones.Clusters.Get(opt.Project, opt.Zone, clusterName).Context(ctx).Do() |
| if err != nil { |
| return nil, fmt.Errorf("cluster %q could not be found in project %q, zone %q: %v", clusterName, opt.Project, opt.Zone, err) |
| } |
| } |
| |
| // Decode certs |
| decode := func(which string, cert string) []byte { |
| if err != nil { |
| return nil |
| } |
| s, decErr := base64.StdEncoding.DecodeString(cert) |
| if decErr != nil { |
| err = fmt.Errorf("error decoding %s cert: %v", which, decErr) |
| } |
| return []byte(s) |
| } |
| clientCert := decode("client cert", cluster.MasterAuth.ClientCertificate) |
| clientKey := decode("client key", cluster.MasterAuth.ClientKey) |
| caCert := decode("cluster cert", cluster.MasterAuth.ClusterCaCertificate) |
| if err != nil { |
| return nil, err |
| } |
| |
| // HTTPS client |
| cert, err := tls.X509KeyPair(clientCert, clientKey) |
| if err != nil { |
| return nil, fmt.Errorf("x509 client key pair could not be generated: %v", err) |
| } |
| |
| // CA Cert from kube master |
| caCertPool := x509.NewCertPool() |
| caCertPool.AppendCertsFromPEM([]byte(caCert)) |
| |
| // Setup TLS config |
| tlsConfig := &tls.Config{ |
| Certificates: []tls.Certificate{cert}, |
| RootCAs: caCertPool, |
| } |
| tlsConfig.BuildNameToCertificate() |
| |
| kubeHTTPClient := &http.Client{ |
| Transport: &http.Transport{ |
| TLSClientConfig: tlsConfig, |
| }, |
| } |
| |
| kubeClient, err := kubernetes.NewClient("https://"+cluster.Endpoint, opt.Namespace, kubeHTTPClient) |
| if err != nil { |
| return nil, fmt.Errorf("kubernetes HTTP client could not be created: %v", err) |
| } |
| return kubeClient, nil |
| } |