client.go 9.53 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

bryanl's avatar
bryanl committed
31
	"github.com/ksonnet/ksonnet/metadata/app"
32 33
	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
// 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.
bryanl's avatar
bryanl committed
55
func NewClientConfig(a app.App, overrides clientcmd.ConfigOverrides, loadingRules clientcmd.ClientConfigLoadingRules) *Config {
56
	config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
bryanl's avatar
bryanl committed
57 58 59 60 61
	return &Config{
		Overrides:    &overrides,
		LoadingRules: &loadingRules,
		Config:       config,
	}
62 63 64 65 66 67 68 69 70
}

// 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)

bryanl's avatar
bryanl committed
71 72 73 74 75
	return &Config{
		Overrides:    &overrides,
		LoadingRules: &loadingRules,
		Config:       config,
	}
76 77 78 79
}

// InitClient initializes a new ClientConfig given the specified environment
// spec and returns the ClientPool, DiscoveryInterface, and namespace.
bryanl's avatar
bryanl committed
80
func InitClient(a app.App, env string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
81
	clientConfig := NewDefaultClientConfig()
bryanl's avatar
bryanl committed
82
	return clientConfig.RestClient(a, &env)
83 84
}

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
// 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,
	}

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
	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
	}

127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
	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{}
146 147
	err = json.Unmarshal(body, &spec)
	if err != nil {
148 149 150 151 152 153 154
		log.Debugf("Failed to parse swagger at %s\n%s", url, err.Error())
		return defaultVersion
	}

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

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
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
}

170 171 172 173 174 175 176
// 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.
bryanl's avatar
bryanl committed
177
func (c *Config) RestClient(a app.App, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
178
	if envName != nil {
bryanl's avatar
bryanl committed
179
		err := c.overrideCluster(a, *envName)
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 221 222 223 224 225 226 227 228
		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.
229
			return "", "", errors.Errorf("No current context found. Make sure a kubeconfig file is present")
230 231 232 233 234 235 236
		}
		// Note: "" is a valid rawConfig.CurrentContext
		context = rawConfig.CurrentContext
	}

	ctx := rawConfig.Contexts[context]
	if ctx == nil {
237
		return "", "", errors.Errorf("context '%s' does not exist in the kubeconfig file", context)
238 239 240 241 242
	}

	log.Infof("Using context '%s' from the kubeconfig file specified at the environment variable $KUBECONFIG", context)
	cluster, exists := rawConfig.Clusters[ctx.Cluster]
	if !exists {
243
		return "", "", errors.Errorf("No cluster with name '%s' exists", ctx.Cluster)
244 245 246 247 248 249 250 251 252 253 254 255
	}

	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.
bryanl's avatar
bryanl committed
256
func (c *Config) overrideCluster(a app.App, envName string) error {
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
	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())
bryanl's avatar
bryanl committed
278
	env, err := a.Environment(envName)
279 280 281 282
	if err != nil {
		return err
	}

bryanl's avatar
bryanl committed
283 284 285
	destination := env.Destination

	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
	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 == "" {
bryanl's avatar
bryanl committed
298 299
				log.Debugf("Overwriting --namespace flag with '%s'", destination.Namespace)
				c.Overrides.Context.Namespace = destination.Namespace
bryanl's avatar
bryanl committed
300 301
			}
			return nil
302
		}
bryanl's avatar
bryanl committed
303 304

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

bryanl's avatar
bryanl committed
308
	c.Overrides.Context.Namespace = destination.Namespace
bryanl's avatar
bryanl committed
309 310 311 312
	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
}