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

30
31
	"github.com/ksonnet/ksonnet/metadata/params"

32
33
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

34
	log "github.com/sirupsen/logrus"
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
42
43
	"github.com/ksonnet/ksonnet/metadata"
	"github.com/ksonnet/ksonnet/template"
	"github.com/ksonnet/ksonnet/utils"
44
45
46

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

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

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

var clientConfig clientcmd.ClientConfig
67
var overrides clientcmd.ConfigOverrides
68
var loadingRules clientcmd.ClientConfigLoadingRules
Angus Lees's avatar
Angus Lees committed
69

70
func init() {
71
	RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
72
73

	// The "usual" clientcmd/kubectl flags
74
	loadingRules = *clientcmd.NewDefaultClientConfigLoadingRules()
75
	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
76
	clientConfig = clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
77
78

	RootCmd.PersistentFlags().Set("logtostderr", "true")
79
80
}

81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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
	cmd.PersistentFlags().StringVar(ep, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster")
	clientcmd.BindOverrideFlags(&overrides, cmd.PersistentFlags(), kflags)
}

98
// RootCmd is the root of cobra subcommand tree
99
var RootCmd = &cobra.Command{
Jessica Yuen's avatar
Jessica Yuen committed
100
	Use:           "ks",
101
102
103
	Short:         "Synchronise Kubernetes resources with config files",
	SilenceErrors: true,
	SilenceUsage:  true,
104
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
105
		goflag.CommandLine.Parse([]string{})
106
107
108
109
110
111
112
113
114
115
116
117
118
119
		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
120
121
122
	},
}

123
124
// clientConfig.Namespace() is broken in client-go 3.0:
// namespace in config erroneously overrides explicit --namespace
125
func namespace() (string, error) {
126
	return namespaceFor(clientConfig, &overrides)
127
128
}

129
func namespaceFor(c clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) (string, error) {
130
131
132
	if overrides.Context.Namespace != "" {
		return overrides.Context.Namespace, nil
	}
133
	ns, _, err := clientConfig.Namespace()
134
135
136
	return ns, err
}

137
// resolveContext returns the server and namespace of the cluster at the provided
138
// context. If context is nil, the current context is used.
139
func resolveContext(context *string) (server, namespace string, err error) {
140
141
142
143
144
145
146
147
148
149
150
	rawConfig, err := clientConfig.RawConfig()
	if err != nil {
		return "", "", err
	}

	ctxName := rawConfig.CurrentContext
	if context != nil {
		ctxName = *context
	}
	ctx := rawConfig.Contexts[ctxName]
	if ctx == nil {
151
152
153
154
155
156
		if len(ctxName) == 0 && ctxName == rawConfig.CurrentContext {
			// User likely does not have a kubeconfig file.
			return "", "", fmt.Errorf("No current context found. Make sure a kubeconfig file is present")
		}

		return "", "", fmt.Errorf("context '%s' does not exist in the kubeconfig file", ctxName)
157
158
159
	}

	log.Infof("Using context '%s'", ctxName)
160
161
162
163
	cluster, exists := rawConfig.Clusters[ctx.Cluster]
	if !exists {
		return "", "", fmt.Errorf("No cluster with name '%s' exists", ctx.Cluster)
	}
164
165
166
	return cluster.Server, ctx.Namespace, nil
}

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
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
}

218
func newExpander(cmd *cobra.Command) (*template.Expander, error) {
219
	flags := cmd.Flags()
220
221
	spec := template.Expander{}
	var err error
222

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

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

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

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

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

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

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

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

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

273
func restClient(cmd *cobra.Command, envName *string, config clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
274
	if envName != nil {
275
		err := overrideCluster(*envName, config, overrides)
276
277
278
279
280
		if err != nil {
			return nil, nil, err
		}
	}

281
	conf, err := config.ClientConfig()
Angus Lees's avatar
Angus Lees committed
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
	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
}
298

299
func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
300
	return restClient(cmd, envName, clientConfig, &overrides)
301
302
}

303
304
305
306
307
type envSpec struct {
	env   *string
	files []string
}

308
// addEnvCmdFlags adds the flags that are common to the family of commands
309
// whose form is `[<env>|-f <file-name>]`, e.g., `apply` and `delete`.
310
311
312
313
func addEnvCmdFlags(cmd *cobra.Command) {
	cmd.PersistentFlags().StringArrayP(flagFile, flagFileShort, nil, "Filename or directory that contains the configuration to apply (accepts YAML, JSON, and Jsonnet)")
}

314
// parseEnvCmd parses the family of commands that come in the form `[<env>|-f
315
// <file-name>]`, e.g., `apply` and `delete`.
316
func parseEnvCmd(cmd *cobra.Command, args []string) (*envSpec, error) {
317
318
319
320
	flags := cmd.Flags()

	files, err := flags.GetStringArray(flagFile)
	if err != nil {
321
		return nil, err
322
323
324
325
326
327
328
	}

	var env *string
	if len(args) == 1 {
		env = &args[0]
	}

329
	return &envSpec{env: env, files: files}, nil
330
331
}

332
// overrideCluster ensures that the server specified in the environment is
333
334
335
// associated in the user's kubeconfig file during deployment to a ksonnet
// environment. We will error out if it is not.
//
336
// If the environment server the user is attempting to deploy to is not the current
337
// kubeconfig context, we must manually override the client-go --cluster flag
338
// to ensure we are deploying to the correct cluster.
339
func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) error {
340
	cwd, err := os.Getwd()
341
	if err != nil {
342
		return err
343
	}
344
	wd := metadata.AbsPath(cwd)
345

346
	metadataManager, err := metadata.Find(wd)
347
	if err != nil {
348
		return err
349
350
	}

351
352
353
354
355
	rawConfig, err := clientConfig.RawConfig()
	if err != nil {
		return err
	}

356
	var servers = make(map[string]string)
357
	for name, cluster := range rawConfig.Clusters {
358
359
360
361
362
363
		server, err := utils.NormalizeURL(cluster.Server)
		if err != nil {
			return err
		}

		servers[server] = name
364
365
366
367
	}

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

371
	log.Debugf("Validating deployment at '%s' with server '%v'", envName, reflect.ValueOf(servers).MapKeys())
372
373
374
375
376
	env, err := metadataManager.GetEnvironment(envName)
	if err != nil {
		return err
	}

377
378
379
380
381
382
383
	server, err := utils.NormalizeURL(env.Server)
	if err != nil {
		return err
	}

	if _, ok := servers[server]; ok {
		clusterName := servers[server]
384
385
		log.Debugf("Overwriting --cluster flag with '%s'", clusterName)
		overrides.Context.Cluster = clusterName
386
387
		log.Debugf("Overwriting --namespace flag with '%s'", env.Namespace)
		overrides.Context.Namespace = env.Namespace
388
389
390
		return nil
	}

391
	return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address", envName, env.Server)
392
393
394
395
396
397
398
399
}

// expandEnvCmdObjs finds and expands templates for the family of commands of
// 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.
func expandEnvCmdObjs(cmd *cobra.Command, envSpec *envSpec, cwd metadata.AbsPath) ([]*unstructured.Unstructured, error) {
400
401
402
403
404
	expander, err := newExpander(cmd)
	if err != nil {
		return nil, err
	}

405
406
407
408
409
410
	//
	// Get all filenames that contain templates to expand. Importantly, we need to
	// enforce the form `[<env-name>|-f <file-name>]`; that is, we need to make
	// sure that the user either passed an environment name or a `-f` flag.
	//

411
412
	envPresent := envSpec.env != nil
	filesPresent := len(envSpec.files) > 0
413

414
415
	if !envPresent && !filesPresent {
		return nil, fmt.Errorf("Must specify either an environment or a file list, or both")
416
417
	}

418
	fileNames := envSpec.files
419
	if envPresent {
420
		manager, err := metadata.Find(cwd)
421
422
423
424
		if err != nil {
			return nil, err
		}

Alex Clemmer's avatar
Alex Clemmer committed
425
426
		libPath, vendorPath, envLibPath, envComponentPath, envParamsPath := manager.LibPaths(*envSpec.env)
		expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(envLibPath)}, expander.FlagJpath...)
427

428
429
430
431
432
433
434
435
436
		componentPaths, err := manager.ComponentPaths()
		if err != nil {
			return nil, err
		}

		baseObj := constructBaseObj(componentPaths)
		params := importParams(string(envParamsPath))
		expander.ExtCodes = append([]string{baseObj, params}, expander.ExtCodes...)

437
		if !filesPresent {
438

439
			fileNames = []string{string(envComponentPath)}
440
441
442
443
444
445
446
447
		}
	}

	//
	// Expand templates.
	//

	return expander.Expand(fileNames)
448
}
449
450
451
452
453
454

// constructBaseObj constructs the base Jsonnet object that represents k-v
// pairs of component name -> component imports. For example,
//
//   {
//      foo: import "components/foo.jsonnet"
455
//      "foo-bar": import "components/foo-bar.jsonnet"
456
457
458
459
460
461
462
463
464
465
466
//   }
func constructBaseObj(paths []string) string {
	var obj bytes.Buffer
	obj.WriteString("{\n")
	for _, p := range paths {
		ext := path.Ext(p)
		if path.Ext(p) != ".jsonnet" {
			continue
		}

		name := strings.TrimSuffix(path.Base(p), ext)
467
		name = params.SanitizeComponent(name)
468
469
470
		fmt.Fprintf(&obj, "  %s: import \"%s\",\n", name, p)
	}
	obj.WriteString("}\n")
471
	return fmt.Sprintf("%s=%s", metadata.ComponentsExtCodeKey, obj.String())
472
}
473
474
475
476

func importParams(path string) string {
	return fmt.Sprintf(`%s=import "%s"`, metadata.ParamsExtCodeKey, path)
}