blob: a7ab1ac7d684b6140a103aa658e672c9b1a1f072 [file] [log] [blame]
// 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
}