root.go 16.8 KB
Newer Older
Angus Lees's avatar
Angus Lees committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Copyright 2017 The kubecfg 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.

16
17
18
package cmd

import (
Angus Lees's avatar
Angus Lees committed
19
20
	"bytes"
	"encoding/json"
21
	goflag "flag"
22
	"fmt"
23
	"io"
24
	"os"
25
	"path"
26
	"path/filepath"
27
	"reflect"
Ted Hahn's avatar
Ted Hahn committed
28
	"strings"
29

30
31
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

bryanl's avatar
bryanl committed
32
	"github.com/pkg/errors"
33
	log "github.com/sirupsen/logrus"
bryanl's avatar
bryanl committed
34
	"github.com/spf13/afero"
35
	"github.com/spf13/cobra"
36
	"golang.org/x/crypto/ssh/terminal"
Angus Lees's avatar
Angus Lees committed
37
38
39
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
40

41
	"github.com/ksonnet/ksonnet/metadata"
42
	str "github.com/ksonnet/ksonnet/strings"
43
44
	"github.com/ksonnet/ksonnet/template"
	"github.com/ksonnet/ksonnet/utils"
45
46
47

	// Register auth plugins
	_ "k8s.io/client-go/plugin/pkg/client/auth"
48
49
)

Angus Lees's avatar
Angus Lees committed
50
const (
51
	flagVerbose    = "verbose"
52
	flagJpath      = "jpath"
Angus Lees's avatar
Angus Lees committed
53
	flagExtVar     = "ext-str"
Thomas Hahn's avatar
Thomas Hahn committed
54
	flagExtVarFile = "ext-str-file"
55
56
	flagTlaVar     = "tla-str"
	flagTlaVarFile = "tla-str-file"
57
58
	flagResolver   = "resolve-images"
	flagResolvFail = "resolve-images-error"
Jessica Yuen's avatar
Jessica Yuen committed
59
	flagAPISpec    = "api-spec"
60

61
	// For use in the commands (e.g., diff, apply, delete) that require either an
62
	// environment or the -f flag.
63
64
	flagComponent      = "component"
	flagComponentShort = "c"
Angus Lees's avatar
Angus Lees committed
65
66
)

bryanl's avatar
bryanl committed
67
68
69
70
71
72
var (
	clientConfig clientcmd.ClientConfig
	overrides    clientcmd.ConfigOverrides
	loadingRules clientcmd.ClientConfigLoadingRules
	appFs        afero.Fs
)
Angus Lees's avatar
Angus Lees committed
73

74
func init() {
bryanl's avatar
bryanl committed
75
76
	appFs = afero.NewOsFs()

77
	RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
78
79

	// The "usual" clientcmd/kubectl flags
80
	loadingRules = *clientcmd.NewDefaultClientConfigLoadingRules()
81
	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
82
	clientConfig = clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
83
84

	RootCmd.PersistentFlags().Set("logtostderr", "true")
85
86
}

87
88
89
90
91
92
93
94
95
96
97
98
99
func bindJsonnetFlags(cmd *cobra.Command) {
	cmd.PersistentFlags().StringSliceP(flagJpath, "J", nil, "Additional jsonnet library search path")
	cmd.PersistentFlags().StringSliceP(flagExtVar, "V", nil, "Values of external variables")
	cmd.PersistentFlags().StringSlice(flagExtVarFile, nil, "Read external variable from a file")
	cmd.PersistentFlags().StringSliceP(flagTlaVar, "A", nil, "Values of top level arguments")
	cmd.PersistentFlags().StringSlice(flagTlaVarFile, nil, "Read top level argument from a file")
	cmd.PersistentFlags().String(flagResolver, "noop", "Change implementation of resolveImage native function. One of: noop, registry")
	cmd.PersistentFlags().String(flagResolvFail, "warn", "Action when resolveImage fails. One of ignore,warn,error")
}

func bindClientGoFlags(cmd *cobra.Command) {
	kflags := clientcmd.RecommendedConfigOverrideFlags("")
	ep := &loadingRules.ExplicitPath
Jessica Yao's avatar
Jessica Yao committed
100
	cmd.PersistentFlags().StringVar(ep, "kubeconfig", "", "Path to a kubeconfig file. Alternative to env var $KUBECONFIG.")
101
102
103
	clientcmd.BindOverrideFlags(&overrides, cmd.PersistentFlags(), kflags)
}

104
// RootCmd is the root of cobra subcommand tree
105
var RootCmd = &cobra.Command{
Jessica Yao's avatar
Jessica Yao committed
106
107
	Use:   "ks",
	Short: `Configure your application to deploy to a Kubernetes cluster`,
Jessica Yao's avatar
Jessica Yao committed
108
109
110
111
112
113
	Long: `
You can use the ` + "`ks`" + ` commands to write, share, and deploy your Kubernetes
application configuration to remote clusters.

----
`,
114
115
	SilenceErrors: true,
	SilenceUsage:  true,
116
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
117
		goflag.CommandLine.Parse([]string{})
118
119
120
121
122
123
124
125
126
127
128
129
130
131
		flags := cmd.Flags()
		out := cmd.OutOrStderr()
		log.SetOutput(out)

		logFmt := NewLogFormatter(out)
		log.SetFormatter(logFmt)

		verbosity, err := flags.GetCount(flagVerbose)
		if err != nil {
			return err
		}
		log.SetLevel(logLevel(verbosity))

		return nil
132
133
134
	},
}

135
136
// clientConfig.Namespace() is broken in client-go 3.0:
// namespace in config erroneously overrides explicit --namespace
137
func namespace() (string, error) {
138
	return namespaceFor(clientConfig, &overrides)
139
140
}

141
func namespaceFor(c clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) (string, error) {
142
143
144
	if overrides.Context.Namespace != "" {
		return overrides.Context.Namespace, nil
	}
145
	ns, _, err := clientConfig.Namespace()
146
147
148
	return ns, err
}

149
150
151
152
// 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 resolveContext(context string) (server, namespace string, err error) {
153
154
155
156
157
	rawConfig, err := clientConfig.RawConfig()
	if err != nil {
		return "", "", err
	}

158
159
	// use the default context where context is empty
	if context == "" {
160
		if rawConfig.CurrentContext == "" && len(rawConfig.Clusters) == 0 {
161
162
163
			// User likely does not have a kubeconfig file.
			return "", "", fmt.Errorf("No current context found. Make sure a kubeconfig file is present")
		}
164
		// Note: "" is a valid rawConfig.CurrentContext
165
166
		context = rawConfig.CurrentContext
	}
167

168
169
170
	ctx := rawConfig.Contexts[context]
	if ctx == nil {
		return "", "", fmt.Errorf("context '%s' does not exist in the kubeconfig file", context)
171
172
	}

173
	log.Infof("Using context '%s' from the kubeconfig file specified at the environment variable $KUBECONFIG", context)
174
175
176
177
	cluster, exists := rawConfig.Clusters[ctx.Cluster]
	if !exists {
		return "", "", fmt.Errorf("No cluster with name '%s' exists", ctx.Cluster)
	}
178

179
180
181
	return cluster.Server, ctx.Namespace, nil
}

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
229
230
231
232
func logLevel(verbosity int) log.Level {
	switch verbosity {
	case 0:
		return log.InfoLevel
	default:
		return log.DebugLevel
	}
}

type logFormatter struct {
	escapes  *terminal.EscapeCodes
	colorise bool
}

// NewLogFormatter creates a new log.Formatter customised for writer
func NewLogFormatter(out io.Writer) log.Formatter {
	var ret = logFormatter{}
	if f, ok := out.(*os.File); ok {
		ret.colorise = terminal.IsTerminal(int(f.Fd()))
		ret.escapes = terminal.NewTerminal(f, "").Escape
	}
	return &ret
}

func (f *logFormatter) levelEsc(level log.Level) []byte {
	switch level {
	case log.DebugLevel:
		return []byte{}
	case log.WarnLevel:
		return f.escapes.Yellow
	case log.ErrorLevel, log.FatalLevel, log.PanicLevel:
		return f.escapes.Red
	default:
		return f.escapes.Blue
	}
}

func (f *logFormatter) Format(e *log.Entry) ([]byte, error) {
	buf := bytes.Buffer{}
	if f.colorise {
		buf.Write(f.levelEsc(e.Level))
		fmt.Fprintf(&buf, "%-5s ", strings.ToUpper(e.Level.String()))
		buf.Write(f.escapes.Reset)
	}

	buf.WriteString(strings.TrimSpace(e.Message))
	buf.WriteString("\n")

	return buf.Bytes(), nil
}

bryanl's avatar
bryanl committed
233
func newExpander(fs afero.Fs, cmd *cobra.Command) (*template.Expander, error) {
234
	flags := cmd.Flags()
bryanl's avatar
bryanl committed
235
	spec := template.NewExpander(fs)
236
	var err error
237

238
	spec.EnvJPath = filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
239

Alex Clemmer's avatar
Fix #95  
Alex Clemmer committed
240
	spec.FlagJpath, err = flags.GetStringSlice(flagJpath)
241
242
243
244
	if err != nil {
		return nil, err
	}

245
	spec.ExtVars, err = flags.GetStringSlice(flagExtVar)
Ted Hahn's avatar
Ted Hahn committed
246
247
248
	if err != nil {
		return nil, err
	}
Thomas Hahn's avatar
Thomas Hahn committed
249

250
	spec.ExtVarFiles, err = flags.GetStringSlice(flagExtVarFile)
251
252
253
254
	if err != nil {
		return nil, err
	}

255
	spec.TlaVars, err = flags.GetStringSlice(flagTlaVar)
256
257
258
259
	if err != nil {
		return nil, err
	}

260
	spec.TlaVarFiles, err = flags.GetStringSlice(flagTlaVarFile)
261
262
263
	if err != nil {
		return nil, err
	}
264

265
	spec.Resolver, err = flags.GetString(flagResolver)
266
267
268
	if err != nil {
		return nil, err
	}
269
	spec.FailAction, err = flags.GetString(flagResolvFail)
270
271
272
273
	if err != nil {
		return nil, err
	}

274
	return &spec, nil
275
}
Angus Lees's avatar
Angus Lees committed
276
277
278
279
280
281
282
283
284
285
286
287

// For debugging
func dumpJSON(v interface{}) string {
	buf := bytes.NewBuffer(nil)
	enc := json.NewEncoder(buf)
	enc.SetIndent("", "  ")
	if err := enc.Encode(v); err != nil {
		return err.Error()
	}
	return string(buf.Bytes())
}

288
func restClient(cmd *cobra.Command, envName *string, config clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
289
	if envName != nil {
290
		err := overrideCluster(*envName, config, overrides)
291
292
293
294
295
		if err != nil {
			return nil, nil, err
		}
	}

296
	conf, err := config.ClientConfig()
Angus Lees's avatar
Angus Lees committed
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
	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)
	return pool, discoCache, nil
}
313

314
func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
315
	return restClient(cmd, envName, clientConfig, &overrides)
316
317
}

318
// addEnvCmdFlags adds the flags that are common to the family of commands
319
// whose form is `[<env>|-f <file-name>]`, e.g., `apply` and `delete`.
320
func addEnvCmdFlags(cmd *cobra.Command) {
321
	cmd.PersistentFlags().StringArrayP(flagComponent, flagComponentShort, nil, "Name of a specific component (multiple -c flags accepted, allows YAML, JSON, and Jsonnet)")
322
323
}

324
// overrideCluster ensures that the server specified in the environment is
325
326
327
// associated in the user's kubeconfig file during deployment to a ksonnet
// environment. We will error out if it is not.
//
328
// If the environment server the user is attempting to deploy to is not the current
329
// kubeconfig context, we must manually override the client-go --cluster flag
330
// to ensure we are deploying to the correct cluster.
331
func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) error {
332
	cwd, err := os.Getwd()
333
	if err != nil {
334
		return err
335
336
	}

337
	metadataManager, err := metadata.Find(cwd)
338
	if err != nil {
339
		return err
340
341
	}

342
343
344
345
346
	rawConfig, err := clientConfig.RawConfig()
	if err != nil {
		return err
	}

347
	var servers = make(map[string]string)
348
	for name, cluster := range rawConfig.Clusters {
349
		server, err := str.NormalizeURL(cluster.Server)
350
351
352
353
354
		if err != nil {
			return err
		}

		servers[server] = name
355
356
357
358
	}

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

362
	log.Debugf("Validating deployment at '%s' with server '%v'", envName, reflect.ValueOf(servers).MapKeys())
363
364
365
366
367
	env, err := metadataManager.GetEnvironment(envName)
	if err != nil {
		return err
	}

368
	// TODO support multi-cluster deployment.
369
	server, err := str.NormalizeURL(env.Destinations[0].Server)
370
371
372
373
374
375
	if err != nil {
		return err
	}

	if _, ok := servers[server]; ok {
		clusterName := servers[server]
376
377
378
379
380
		if overrides.Context.Cluster == "" {
			log.Debugf("Overwriting --cluster flag with '%s'", clusterName)
			overrides.Context.Cluster = clusterName
		}
		if overrides.Context.Namespace == "" {
381
382
			log.Debugf("Overwriting --namespace flag with '%s'", env.Destinations[0].Namespace)
			overrides.Context.Namespace = env.Destinations[0].Namespace
383
		}
384
385
386
		return nil
	}

387
	return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address", envName, env.Destinations[0].Server)
388
389
}

bryanl's avatar
bryanl committed
390
391
392
393
394
type cmdObjExpanderConfig struct {
	fs         afero.Fs
	cmd        *cobra.Command
	env        string
	components []string
395
	cwd        string
bryanl's avatar
bryanl committed
396
397
398
}

// cmdObjExpander finds and expands templates for the family of commands of
399
400
401
402
// the form `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. That is, if
// the user passes a list of files, we will expand all templates in those files,
// while if a user passes an environment name, we will expand all component
// files using that environment.
bryanl's avatar
bryanl committed
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
type cmdObjExpander struct {
	config             cmdObjExpanderConfig
	templateExpanderFn func(afero.Fs, *cobra.Command) (*template.Expander, error)
}

func newCmdObjExpander(c cmdObjExpanderConfig) *cmdObjExpander {
	if c.fs == nil {
		c.fs = appFs
	}

	return &cmdObjExpander{
		config:             c,
		templateExpanderFn: newExpander,
	}
}

// Expands expands the templates.
func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) {
	expander, err := te.templateExpanderFn(te.config.fs, te.config.cmd)
422
	if err != nil {
bryanl's avatar
bryanl committed
423
		return nil, errors.Wrap(err, "template expander")
424
425
	}

426
	//
427
	// Set up the template expander to be able to expand the ksonnet application.
428
429
	//

bryanl's avatar
bryanl committed
430
	manager, err := metadata.Find(te.config.cwd)
431
	if err != nil {
bryanl's avatar
bryanl committed
432
		return nil, errors.Wrap(err, "find metadata")
433
434
	}

435
436
437
438
439
	_, vendorPath := manager.LibPaths()
	libPath, mainPath, paramsPath, err := manager.EnvPaths(te.config.env)
	if err != nil {
		return nil, err
	}
Jessica Yuen's avatar
Jessica Yuen committed
440

441
	expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...)
442

443
444
	componentPaths, err := manager.ComponentPaths()
	if err != nil {
bryanl's avatar
bryanl committed
445
		return nil, errors.Wrap(err, "component paths")
446
	}
447

bryanl's avatar
bryanl committed
448
	baseObj, err := constructBaseObj(componentPaths, te.config.components)
449
	if err != nil {
bryanl's avatar
bryanl committed
450
		return nil, errors.Wrap(err, "construct base object")
451
	}
Jessica Yuen's avatar
Jessica Yuen committed
452
453
454
455
456
457

	//
	// Set up ExtCodes to resolve runtime variables such as the environment namespace.
	//

	params := importParams(string(paramsPath))
458
459
460
461
462
463
	envSpec, err := importEnv(manager, te.config.env)
	if err != nil {
		return nil, err
	}

	expander.ExtCodes = append([]string{baseObj, params, envSpec}, expander.ExtCodes...)
464
465

	//
466
	// Expand the ksonnet app as rendered for environment `env`.
467
468
	//

Jessica Yuen's avatar
Jessica Yuen committed
469
	return expander.Expand([]string{string(mainPath)})
470
}
471
472
473
474
475
476

// constructBaseObj constructs the base Jsonnet object that represents k-v
// pairs of component name -> component imports. For example,
//
//   {
//      foo: import "components/foo.jsonnet"
477
//      "foo-bar": import "components/foo-bar.jsonnet"
478
//   }
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
func constructBaseObj(componentPaths, componentNames []string) (string, error) {
	// IMPLEMENTATION NOTE: If one or more `componentNames` exist, it is
	// sufficient to simply omit every name that does not appear in the list. This
	// is because we know every field of the base object will contain _only_ an
	// `import` node (see example object in the function-heading comment). This
	// would not be true in cases where one field can reference another field; in
	// this case, one would need to generate the entire object, and filter that.
	//
	// Hence, a word of caution: if the base object ever becomes more complex, you
	// will need to change the way this function performs filtering, as it will
	// lead to very confusing bugs.

	shouldFilter := len(componentNames) > 0
	filter := map[string]string{}
	for _, name := range componentNames {
		filter[name] = ""
	}

	// Add every component we know about to the base object.
498
499
	var obj bytes.Buffer
	obj.WriteString("{\n")
500
	for _, p := range componentPaths {
501
		ext := path.Ext(p)
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
		componentName := strings.TrimSuffix(path.Base(p), ext)

		// Filter! If the filter has more than 1 element and the component name is
		// not in the filter, skip.
		if _, exists := filter[componentName]; shouldFilter && !exists {
			continue
		} else if shouldFilter && exists {
			delete(filter, componentName)
		}

		// Generate import statement.
		var importExpr string
		switch ext {
		case ".jsonnet":
			importExpr = fmt.Sprintf(`import "%s"`, p)

		// TODO: Pull in YAML and JSON when we build the base object.
		//
		// case ".yaml", ".yml":
		// 	importExpr = fmt.Sprintf(`util.parseYaml("%s")`, p)
		// case ".json":
		// 	importExpr = fmt.Sprintf(`util.parseJson("%s")`, p)
		default:
525
526
527
			continue
		}

528
529
		// Emit object field. Sanitize the name to guarantee we generate valid
		// Jsonnet.
530
		componentName = str.QuoteNonASCII(componentName)
531
		fmt.Fprintf(&obj, "  %s: %s,\n", componentName, importExpr)
532
	}
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547

	// Check that we found all the components the user asked for.
	if shouldFilter && len(filter) != 0 {
		names := []string{}
		for name := range filter {
			names = append(names, "'"+name+"'")
		}
		return "", fmt.Errorf("Failed to filter components; the following components don't exist: [ %s ]", strings.Join(names, ","))
	}

	// Terminate object.
	fmt.Fprintf(&obj, "}\n")

	// Emit `base.libsonnet`.
	return fmt.Sprintf("%s=%s", metadata.ComponentsExtCodeKey, obj.String()), nil
548
}
549
550
551
552

func importParams(path string) string {
	return fmt.Sprintf(`%s=import "%s"`, metadata.ParamsExtCodeKey, path)
}
Jessica Yuen's avatar
Jessica Yuen committed
553

554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
func importEnv(manager metadata.Manager, env string) (string, error) {
	app, err := manager.AppSpec()
	if err != nil {
		return "", err
	}

	spec, exists := app.GetEnvironmentSpec(env)
	if !exists {
		return "", fmt.Errorf("Environment '%s' does not exist in app.yaml", env)
	}

	// TODO pass namespace and server as params when ks supports multi-cluster deployment
	type EnvironmentSpec struct {
		Server    string `json:"server"`
		Namespace string `json:"namespace"`
	}

	toMarshal := &EnvironmentSpec{
		Server:    spec.Destinations[0].Server,
		Namespace: spec.Destinations[0].Namespace,
	}

	marshalled, err := json.Marshal(toMarshal)
	if err != nil {
		return "", err
	}

	return fmt.Sprintf(`%s=%s`, metadata.EnvExtCodeKey, string(marshalled)), nil
Jessica Yuen's avatar
Jessica Yuen committed
582
}