root.go 13 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"
Ted Hahn's avatar
Ted Hahn committed
27
	"strings"
28

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

31
32
	"github.com/ksonnet/ksonnet/component"
	"github.com/ksonnet/ksonnet/metadata"
bryanl's avatar
bryanl committed
33
	"github.com/ksonnet/ksonnet/plugin"
34
35
	str "github.com/ksonnet/ksonnet/strings"
	"github.com/ksonnet/ksonnet/template"
bryanl's avatar
bryanl committed
36
	"github.com/pkg/errors"
37
	log "github.com/sirupsen/logrus"
bryanl's avatar
bryanl committed
38
	"github.com/spf13/afero"
39
	"github.com/spf13/cobra"
40
	"golang.org/x/crypto/ssh/terminal"
41
42
43

	// Register auth plugins
	_ "k8s.io/client-go/plugin/pkg/client/auth"
44
45
)

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

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

bryanl's avatar
bryanl committed
63
var (
64
	appFs afero.Fs
bryanl's avatar
bryanl committed
65
)
Angus Lees's avatar
Angus Lees committed
66

67
func init() {
bryanl's avatar
bryanl committed
68
69
	appFs = afero.NewOsFs()

70
	RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
71
	RootCmd.PersistentFlags().Set("logtostderr", "true")
72
73
}

74
75
76
77
78
79
80
81
82
83
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")
}

84
// RootCmd is the root of cobra subcommand tree
85
var RootCmd = &cobra.Command{
Jessica Yao's avatar
Jessica Yao committed
86
87
	Use:   "ks",
	Short: `Configure your application to deploy to a Kubernetes cluster`,
Jessica Yao's avatar
Jessica Yao committed
88
89
90
91
92
93
	Long: `
You can use the ` + "`ks`" + ` commands to write, share, and deploy your Kubernetes
application configuration to remote clusters.

----
`,
94
95
	SilenceErrors: true,
	SilenceUsage:  true,
96
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
97
		goflag.CommandLine.Parse([]string{})
98
99
100
101
102
103
104
105
106
107
108
109
110
111
		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
112
	},
bryanl's avatar
bryanl committed
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
	Args: func(cmd *cobra.Command, args []string) error {
		if len(args) == 0 {
			return cobra.NoArgs(cmd, args)
		}

		pluginName := args[0]
		_, err := plugin.Find(appFs, pluginName)
		if err != nil {
			return cobra.NoArgs(cmd, args)
		}

		return nil
	},
	RunE: func(cmd *cobra.Command, args []string) error {
		if len(args) == 0 {
			return cmd.Help()
		}
		pluginName, args := args[0], args[1:]
		p, err := plugin.Find(appFs, pluginName)
		if err != nil {
			return err
		}

		return runPlugin(p, args)
	},
}

func runPlugin(p plugin.Plugin, args []string) error {
	env := []string{
		fmt.Sprintf("KS_PLUGIN_DIR=%s", p.RootDir),
		fmt.Sprintf("KS_PLUGIN_NAME=%s", p.Config.Name),
	}

	root, err := appRoot()
	if err != nil {
		return err
	}

	appConfig := filepath.Join(root, "app.yaml")
	exists, err := afero.Exists(appFs, appConfig)
	if err != nil {
		return err
	}

	if exists {
		env = append(env, fmt.Sprintf("KS_APP_DIR=%s", root))
		// TODO: make kube context or something similar available to the plugin
	}

	cmd := p.BuildRunCmd(env, args)
	return cmd.Run()
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
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
217
func newExpander(fs afero.Fs, cmd *cobra.Command) (*template.Expander, error) {
218
	flags := cmd.Flags()
bryanl's avatar
bryanl committed
219
	spec := template.NewExpander(fs)
220
	var err error
221

222
	spec.EnvJPath = filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
223

Alex Clemmer's avatar
Fix #95  
Alex Clemmer committed
224
	spec.FlagJpath, err = flags.GetStringSlice(flagJpath)
225
226
227
228
	if err != nil {
		return nil, err
	}

229
	spec.ExtVars, err = flags.GetStringSlice(flagExtVar)
Ted Hahn's avatar
Ted Hahn committed
230
231
232
	if err != nil {
		return nil, err
	}
Thomas Hahn's avatar
Thomas Hahn committed
233

234
	spec.ExtVarFiles, err = flags.GetStringSlice(flagExtVarFile)
235
236
237
238
	if err != nil {
		return nil, err
	}

239
	spec.TlaVars, err = flags.GetStringSlice(flagTlaVar)
240
241
242
243
	if err != nil {
		return nil, err
	}

244
	spec.TlaVarFiles, err = flags.GetStringSlice(flagTlaVarFile)
245
246
247
	if err != nil {
		return nil, err
	}
248

249
	spec.Resolver, err = flags.GetString(flagResolver)
250
251
252
	if err != nil {
		return nil, err
	}
253
	spec.FailAction, err = flags.GetString(flagResolvFail)
254
255
256
257
	if err != nil {
		return nil, err
	}

258
	return &spec, nil
259
}
Angus Lees's avatar
Angus Lees committed
260
261
262
263
264
265
266
267
268
269
270
271

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

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

bryanl's avatar
bryanl committed
278
279
280
281
282
type cmdObjExpanderConfig struct {
	fs         afero.Fs
	cmd        *cobra.Command
	env        string
	components []string
283
	cwd        string
bryanl's avatar
bryanl committed
284
285
286
}

// cmdObjExpander finds and expands templates for the family of commands of
287
288
289
290
// 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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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)
310
	if err != nil {
bryanl's avatar
bryanl committed
311
		return nil, errors.Wrap(err, "template expander")
312
313
	}

314
	//
315
	// Set up the template expander to be able to expand the ksonnet application.
316
317
	//

bryanl's avatar
bryanl committed
318
	manager, err := metadata.Find(te.config.cwd)
319
	if err != nil {
bryanl's avatar
bryanl committed
320
		return nil, errors.Wrap(err, "find metadata")
321
322
	}

323
	_, vendorPath := manager.LibPaths()
bryanl's avatar
bryanl committed
324
	libPath, mainPath, _, err := manager.EnvPaths(te.config.env)
325
326
327
	if err != nil {
		return nil, err
	}
Jessica Yuen's avatar
Jessica Yuen committed
328

329
	expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...)
330

bryanl's avatar
bryanl committed
331
	namespacedComponentPaths, err := component.MakePathsByNamespace(te.config.fs, manager, te.config.cwd, te.config.env)
332
	if err != nil {
bryanl's avatar
bryanl committed
333
		return nil, errors.Wrap(err, "component paths")
334
	}
335

Jessica Yuen's avatar
Jessica Yuen committed
336
337
338
339
	//
	// Set up ExtCodes to resolve runtime variables such as the environment namespace.
	//

340
341
342
343
344
	envSpec, err := importEnv(manager, te.config.env)
	if err != nil {
		return nil, err
	}

bryanl's avatar
bryanl committed
345
	baseCodes := expander.ExtCodes
346

bryanl's avatar
bryanl committed
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
	slUnstructured := make([]*unstructured.Unstructured, 0)
	for ns, componentPaths := range namespacedComponentPaths {

		paramsPath := ns.ParamsPath()
		params := importParams(string(paramsPath))

		baseObj, err := constructBaseObj(componentPaths, te.config.components)
		if err != nil {
			return nil, errors.Wrap(err, "construct base object")
		}

		//
		// Expand the ksonnet app as rendered for environment `env`.
		//
		expander.ExtCodes = append([]string{baseObj, params, envSpec}, baseCodes...)
		u, err := expander.Expand([]string{string(mainPath)})
		if err != nil {
			return nil, errors.Wrapf(err, "generate objects for namespace %s", ns.Path)
		}

		slUnstructured = append(slUnstructured, u...)
	}

	return slUnstructured, nil
371

372
}
373
374
375
376
377
378

// constructBaseObj constructs the base Jsonnet object that represents k-v
// pairs of component name -> component imports. For example,
//
//   {
//      foo: import "components/foo.jsonnet"
379
//      "foo-bar": import "components/foo-bar.jsonnet"
380
//   }
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
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.
400
401
	var obj bytes.Buffer
	obj.WriteString("{\n")
402
	for _, p := range componentPaths {
403
		ext := path.Ext(p)
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
		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:
427
428
429
			continue
		}

430
431
		// Emit object field. Sanitize the name to guarantee we generate valid
		// Jsonnet.
432
		componentName = str.QuoteNonASCII(componentName)
433
		fmt.Fprintf(&obj, "  %s: %s,\n", componentName, importExpr)
434
	}
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449

	// 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
450
}
451
452
453
454

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

456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
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)
	}

	type EnvironmentSpec struct {
		Server    string `json:"server"`
		Namespace string `json:"namespace"`
	}

	toMarshal := &EnvironmentSpec{
473
474
		Server:    spec.Destination.Server,
		Namespace: spec.Destination.Namespace,
475
476
477
478
479
480
481
482
	}

	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
483
}
bryanl's avatar
bryanl committed
484
485
486
487

func appRoot() (string, error) {
	return os.Getwd()
}