client.go 9.58 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// Copyright 2017 The ksonnet 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 client

import (
19 20
	"crypto/tls"
	"crypto/x509"
21
	"encoding/json"
22
	"fmt"
23 24 25
	"io/ioutil"
	"net/http"
	"net/url"
26
	"os"
27
	"path"
28
	"reflect"
29
	"time"
30 31 32 33

	"github.com/ksonnet/ksonnet/metadata"
	str "github.com/ksonnet/ksonnet/strings"
	"github.com/ksonnet/ksonnet/utils"
34
	"github.com/pkg/errors"
35 36 37 38 39 40 41
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
)

42 43 44 45
const (
	defaultVersion = "version:v1.7.0"
)

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
// Config is a wrapper around client-go's ClientConfig
type Config struct {
	Overrides    *clientcmd.ConfigOverrides
	LoadingRules *clientcmd.ClientConfigLoadingRules

	Config clientcmd.ClientConfig
}

// NewClientConfig initializes a new client.Config with the provided loading rules and overrides.
func NewClientConfig(overrides clientcmd.ConfigOverrides, loadingRules clientcmd.ClientConfigLoadingRules) *Config {
	config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
	return &Config{Overrides: &overrides, LoadingRules: &loadingRules, Config: config}
}

// NewDefaultClientConfig initializes a new ClientConfig with default loading rules and no overrides.
func NewDefaultClientConfig() *Config {
	overrides := clientcmd.ConfigOverrides{}
	loadingRules := *clientcmd.NewDefaultClientConfigLoadingRules()
	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
	config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)

	return &Config{Overrides: &overrides, LoadingRules: &loadingRules, Config: config}
}

// InitClient initializes a new ClientConfig given the specified environment
// spec and returns the ClientPool, DiscoveryInterface, and namespace.
func InitClient(env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
	clientConfig := NewDefaultClientConfig()
	return clientConfig.RestClient(&env)
}

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
// GetAPISpec reads the kubernetes API version from this client's swagger.json.
// We anticipate the swagger.json to be located at <server>/swagger.json.
// If no swagger is found, or we are unable to authenticate to the server, we
// will default to version:v1.7.0.
func (c *Config) GetAPISpec(server string) string {
	type Info struct {
		Version string `json:"version"`
	}

	type Spec struct {
		Info Info `json:"info"`
	}

	u, err := url.Parse(server)
	u.Path = path.Join(u.Path, "swagger.json")
	url := u.String()

	client := http.Client{
		Timeout: time.Second * 2,
	}

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
	restConfig, err := c.Config.ClientConfig()
	if err != nil {
		log.Debugf("Failed to retrieve REST config:\n%v", err)
	}

	if len(restConfig.TLSClientConfig.CAData) > 0 {
		log.Info("Configuring TLS (from data) for retrieving cluster swagger.json")
		client.Transport = buildTransportFromData(restConfig.TLSClientConfig.CAData)
	}

	if restConfig.TLSClientConfig.CAFile != "" {
		log.Info("Configuring TLS (from file) for retrieving cluster swagger.json")
		transport, err := buildTransportFromFile(restConfig.TLSClientConfig.CAFile)
		if err != nil {
			log.Debugf("Failed to read CA file: %v", err)
			return defaultVersion
		}

		client.Transport = transport
	}

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		log.Debugf("Failed to create request at %s\n%s", url, err.Error())
		return defaultVersion
	}

	res, err := client.Do(req)
	if err != nil {
		log.Debugf("Failed to open swagger at %s\n%s", url, err.Error())
		return defaultVersion
	}

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Debugf("Failed to read swagger at %s\n%s", url, err.Error())
		return defaultVersion
	}

	spec := Spec{}
138 139
	err = json.Unmarshal(body, &spec)
	if err != nil {
140 141 142 143 144 145 146
		log.Debugf("Failed to parse swagger at %s\n%s", url, err.Error())
		return defaultVersion
	}

	return fmt.Sprintf("version:%s", spec.Info.Version)
}

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
func buildTransportFromData(data []byte) *http.Transport {
	tlsConfig := &tls.Config{RootCAs: x509.NewCertPool()}
	tlsConfig.RootCAs.AppendCertsFromPEM(data)
	return &http.Transport{TLSClientConfig: tlsConfig}
}

func buildTransportFromFile(file string) (*http.Transport, error) {
	data, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, errors.Wrap(err, "unable to ready CA file")
	}

	return buildTransportFromData(data), nil
}

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
// Namespace returns the namespace for the provided ClientConfig.
func (c *Config) Namespace() (string, error) {
	ns, _, err := c.Config.Namespace()
	return ns, err
}

// RestClient returns the ClientPool, DiscoveryInterface, and Namespace based on the environment spec.
func (c *Config) RestClient(envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
	if envName != nil {
		err := c.overrideCluster(*envName)
		if err != nil {
			return nil, nil, "", err
		}
	}

	conf, err := c.Config.ClientConfig()
	if err != nil {
		return nil, nil, "", err
	}

	disco, err := discovery.NewDiscoveryClientForConfig(conf)
	if err != nil {
		return nil, nil, "", err
	}

	discoCache := utils.NewMemcachedDiscoveryClient(disco)
	mapper := discovery.NewDeferredDiscoveryRESTMapper(discoCache, dynamic.VersionInterfaces)
	pathresolver := dynamic.LegacyAPIPathResolverFunc

	pool := dynamic.NewClientPool(conf, mapper, pathresolver)

	ns, err := c.Namespace()
	if err != nil {
		return nil, nil, "", err
	}

	return pool, discoCache, ns, nil
}

// BindClientGoFlags binds client-go flags to the specified command. This way
// any overrides to client-go flags will automatically update the client config.
func (c *Config) BindClientGoFlags(cmd *cobra.Command) {
	cmd.PersistentFlags().StringVar(&c.LoadingRules.ExplicitPath, "kubeconfig", "", "Path to a kubeconfig file. Alternative to env var $KUBECONFIG.")
	clientcmd.BindOverrideFlags(c.Overrides, cmd.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags(""))
}

// ResolveContext returns the server and namespace of the cluster at the
// provided context. If the context string is empty, the "default" context is
// used.
func (c *Config) ResolveContext(context string) (server, namespace string, err error) {
	rawConfig, err := c.Config.RawConfig()
	if err != nil {
		return "", "", err
	}

	// use the default context where context is empty
	if context == "" {
		if rawConfig.CurrentContext == "" && len(rawConfig.Clusters) == 0 {
			// User likely does not have a kubeconfig file.
221
			return "", "", errors.Errorf("No current context found. Make sure a kubeconfig file is present")
222 223 224 225 226 227 228
		}
		// Note: "" is a valid rawConfig.CurrentContext
		context = rawConfig.CurrentContext
	}

	ctx := rawConfig.Contexts[context]
	if ctx == nil {
229
		return "", "", errors.Errorf("context '%s' does not exist in the kubeconfig file", context)
230 231 232 233 234
	}

	log.Infof("Using context '%s' from the kubeconfig file specified at the environment variable $KUBECONFIG", context)
	cluster, exists := rawConfig.Clusters[ctx.Cluster]
	if !exists {
235
		return "", "", errors.Errorf("No cluster with name '%s' exists", ctx.Cluster)
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
	}

	return cluster.Server, ctx.Namespace, nil
}

// overrideCluster ensures that the server specified in the environment is
// associated in the user's kubeconfig file during deployment to a ksonnet
// environment. We will error out if it is not.
//
// If the environment server the user is attempting to deploy to is not the current
// kubeconfig context, we must manually override the client-go --cluster flag
// to ensure we are deploying to the correct cluster.
func (c *Config) overrideCluster(envName string) error {
	cwd, err := os.Getwd()
	if err != nil {
		return err
	}

	metadataManager, err := metadata.Find(cwd)
	if err != nil {
		return err
	}

	rawConfig, err := c.Config.RawConfig()
	if err != nil {
		return err
	}

	var servers = make(map[string]string)
	for name, cluster := range rawConfig.Clusters {
		server, err := str.NormalizeURL(cluster.Server)
		if err != nil {
			return err
		}

		servers[server] = name
	}

	//
	// check to ensure that the environment we are trying to deploy to is
	// created, and that the server is located in kubeconfig.
	//

	log.Debugf("Validating deployment at '%s' with server '%v'", envName, reflect.ValueOf(servers).MapKeys())
280
	destination, err := metadataManager.GetDestination(envName)
281 282 283 284
	if err != nil {
		return err
	}

285
	server, err := str.NormalizeURL(destination.Server())
286 287 288 289
	if err != nil {
		return err
	}

bryanl's avatar
bryanl committed
290 291 292 293 294 295 296 297 298 299 300 301
	if len(servers) > 0 {
		if _, ok := servers[server]; ok {
			clusterName := servers[server]
			if c.Overrides.Context.Cluster == "" {
				log.Debugf("Overwriting --cluster flag with '%s'", clusterName)
				c.Overrides.Context.Cluster = clusterName
			}
			if c.Overrides.Context.Namespace == "" {
				log.Debugf("Overwriting --namespace flag with '%s'", destination.Namespace())
				c.Overrides.Context.Namespace = destination.Namespace()
			}
			return nil
302
		}
bryanl's avatar
bryanl committed
303 304 305

		return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address",
			envName, destination.Server())
306 307
	}

bryanl's avatar
bryanl committed
308 309 310 311 312
	c.Overrides.Context.Namespace = destination.Namespace()
	c.Overrides.ClusterInfo.Server = server
	// NOTE: ignore TLS verify since we don't have a CA cert to verify with.
	c.Overrides.ClusterInfo.InsecureSkipTLSVerify = true
	return nil
313
}