root.go 11.5 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
	"github.com/ksonnet/ksonnet/metadata"
bryanl's avatar
bryanl committed
32
	"github.com/ksonnet/ksonnet/metadata/app"
bryanl's avatar
bryanl committed
33
	"github.com/ksonnet/ksonnet/pkg/env"
bryanl's avatar
bryanl committed
34
	"github.com/ksonnet/ksonnet/pkg/pipeline"
bryanl's avatar
bryanl committed
35
	"github.com/ksonnet/ksonnet/plugin"
36 37
	str "github.com/ksonnet/ksonnet/strings"
	"github.com/ksonnet/ksonnet/template"
bryanl's avatar
bryanl committed
38
	"github.com/pkg/errors"
39
	log "github.com/sirupsen/logrus"
bryanl's avatar
bryanl committed
40
	"github.com/spf13/afero"
41
	"github.com/spf13/cobra"
42
	"golang.org/x/crypto/ssh/terminal"
43 44 45

	// Register auth plugins
	_ "k8s.io/client-go/plugin/pkg/client/auth"
46 47
)

bryanl's avatar
bryanl committed
48
var (
bryanl's avatar
bryanl committed
49
	appFs = afero.NewOsFs()
50
	ka    app.App
bryanl's avatar
bryanl committed
51
)
Angus Lees's avatar
Angus Lees committed
52

53
func init() {
54
	RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
55
	RootCmd.PersistentFlags().Set("logtostderr", "true")
56 57
}

58 59 60 61 62 63 64 65 66 67
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")
}

68
// RootCmd is the root of cobra subcommand tree
69
var RootCmd = &cobra.Command{
Jessica Yao's avatar
Jessica Yao committed
70 71
	Use:   "ks",
	Short: `Configure your application to deploy to a Kubernetes cluster`,
Jessica Yao's avatar
Jessica Yao committed
72 73 74 75 76 77
	Long: `
You can use the ` + "`ks`" + ` commands to write, share, and deploy your Kubernetes
application configuration to remote clusters.

----
`,
78 79
	SilenceErrors: true,
	SilenceUsage:  true,
80
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
81
		goflag.CommandLine.Parse([]string{})
82 83 84 85 86 87 88 89 90 91 92 93 94
		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))

95 96 97 98 99
		wd, err := os.Getwd()
		if err != nil {
			return err
		}

100 101 102
		var isInit bool
		if len(args) == 2 && args[0] == "init" {
			isInit = true
103 104
		}

105 106 107
		ka, err = app.Load(appFs, wd)
		if err != nil && isInit {
			return err
108 109
		}

110
		return nil
111
	},
bryanl's avatar
bryanl committed
112 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
	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),
bryanl's avatar
bryanl committed
143
		fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
bryanl's avatar
bryanl committed
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
	}

	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) {
bryanl's avatar
bryanl committed
275
	cmd.PersistentFlags().StringSliceP(flagComponent, shortComponent, 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
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) {
bryanl's avatar
bryanl committed
309 310 311 312
	// expander, err := te.templateExpanderFn(te.config.fs, te.config.cmd)
	// if err != nil {
	// 	return nil, errors.Wrap(err, "template expander")
	// }
313

bryanl's avatar
bryanl committed
314
	manager, err := metadata.Find(te.config.cwd)
315
	if err != nil {
bryanl's avatar
bryanl committed
316
		return nil, errors.Wrap(err, "find metadata")
317 318
	}

bryanl's avatar
bryanl committed
319
	ksApp, err := manager.App()
320 321 322
	if err != nil {
		return nil, err
	}
Jessica Yuen's avatar
Jessica Yuen committed
323

bryanl's avatar
bryanl committed
324 325
	p := pipeline.New(ksApp, te.config.env)
	return p.Objects(te.config.components)
326
}
327 328 329 330 331 332

// constructBaseObj constructs the base Jsonnet object that represents k-v
// pairs of component name -> component imports. For example,
//
//   {
//      foo: import "components/foo.jsonnet"
333
//      "foo-bar": import "components/foo-bar.jsonnet"
334
//   }
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
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.
354 355
	var obj bytes.Buffer
	obj.WriteString("{\n")
356
	for _, p := range componentPaths {
357
		ext := path.Ext(p)
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
		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:
381 382 383
			continue
		}

384 385
		// Emit object field. Sanitize the name to guarantee we generate valid
		// Jsonnet.
386
		componentName = str.QuoteNonASCII(componentName)
387
		fmt.Fprintf(&obj, "  %s: %s,\n", componentName, importExpr)
388
	}
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403

	// 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
404
}
405 406 407 408

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

410 411
func importEnv(manager metadata.Manager, envName string) (string, error) {
	app, err := manager.App()
412 413 414 415
	if err != nil {
		return "", err
	}

416 417 418
	spec, err := app.Environment(envName)
	if err != nil {
		return "", fmt.Errorf("Environment '%s' does not exist in app.yaml", envName)
419 420
	}

421
	destination := env.NewDestination(spec.Destination.Server, spec.Destination.Namespace)
422

423
	marshalled, err := json.Marshal(&destination)
424 425 426 427 428
	if err != nil {
		return "", err
	}

	return fmt.Sprintf(`%s=%s`, metadata.EnvExtCodeKey, string(marshalled)), nil
Jessica Yuen's avatar
Jessica Yuen committed
429
}
bryanl's avatar
bryanl committed
430 431 432 433

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