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"
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
151
152
153
154
155
156
157
158
	rawConfig, err := clientConfig.RawConfig()
	if err != nil {
		return "", "", err
	}

	ctxName := rawConfig.CurrentContext
	if context != nil {
		ctxName = *context
	}
	ctx := rawConfig.Contexts[ctxName]
	if ctx == nil {
		return "", "", fmt.Errorf("context '%s' does not exist in the kubeconfig file", *context)
	}

	log.Infof("Using context '%s'", ctxName)
	cluster := rawConfig.Clusters[ctx.Cluster]
	return cluster.Server, ctx.Namespace, nil
}

159
160
161
162
163
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
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
}

210
func newExpander(cmd *cobra.Command) (*template.Expander, error) {
211
	flags := cmd.Flags()
212
213
	spec := template.Expander{}
	var err error
214

215
	spec.EnvJPath = filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
216

Alex Clemmer's avatar
Fix #95  
Alex Clemmer committed
217
	spec.FlagJpath, err = flags.GetStringSlice(flagJpath)
218
219
220
221
	if err != nil {
		return nil, err
	}

222
	spec.ExtVars, err = flags.GetStringSlice(flagExtVar)
Ted Hahn's avatar
Ted Hahn committed
223
224
225
	if err != nil {
		return nil, err
	}
Thomas Hahn's avatar
Thomas Hahn committed
226

227
	spec.ExtVarFiles, err = flags.GetStringSlice(flagExtVarFile)
228
229
230
231
	if err != nil {
		return nil, err
	}

232
	spec.TlaVars, err = flags.GetStringSlice(flagTlaVar)
233
234
235
236
	if err != nil {
		return nil, err
	}

237
	spec.TlaVarFiles, err = flags.GetStringSlice(flagTlaVarFile)
238
239
240
	if err != nil {
		return nil, err
	}
241

242
	spec.Resolver, err = flags.GetString(flagResolver)
243
244
245
	if err != nil {
		return nil, err
	}
246
	spec.FailAction, err = flags.GetString(flagResolvFail)
247
248
249
250
	if err != nil {
		return nil, err
	}

251
	return &spec, nil
252
}
Angus Lees's avatar
Angus Lees committed
253
254
255
256
257
258
259
260
261
262
263
264

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

265
func restClient(cmd *cobra.Command, envName *string, config clientcmd.ClientConfig, overrides *clientcmd.ConfigOverrides) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
266
	if envName != nil {
267
		err := overrideCluster(*envName, config, overrides)
268
269
270
271
272
		if err != nil {
			return nil, nil, err
		}
	}

273
	conf, err := config.ClientConfig()
Angus Lees's avatar
Angus Lees committed
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
	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
}
290

291
func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
292
	return restClient(cmd, envName, clientConfig, &overrides)
293
294
}

295
296
297
298
299
type envSpec struct {
	env   *string
	files []string
}

300
// addEnvCmdFlags adds the flags that are common to the family of commands
301
// whose form is `[<env>|-f <file-name>]`, e.g., `apply` and `delete`.
302
303
304
305
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)")
}

306
// parseEnvCmd parses the family of commands that come in the form `[<env>|-f
307
// <file-name>]`, e.g., `apply` and `delete`.
308
func parseEnvCmd(cmd *cobra.Command, args []string) (*envSpec, error) {
309
310
311
312
	flags := cmd.Flags()

	files, err := flags.GetStringArray(flagFile)
	if err != nil {
313
		return nil, err
314
315
316
317
318
319
320
	}

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

321
	return &envSpec{env: env, files: files}, nil
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
	wd := metadata.AbsPath(cwd)
337

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

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

348
	var servers = make(map[string]string)
349
	for name, cluster := range rawConfig.Clusters {
350
		servers[cluster.Server] = name
351
352
353
354
	}

	//
	// check to ensure that the environment we are trying to deploy to is
355
	// created, and that the server is located in kubeconfig.
356
357
	//

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

364
365
	if _, ok := servers[env.Server]; ok {
		clusterName := servers[env.Server]
366
367
		log.Debugf("Overwriting --cluster flag with '%s'", clusterName)
		overrides.Context.Cluster = clusterName
368
369
		log.Debugf("Overwriting --namespace flag with '%s'", env.Namespace)
		overrides.Context.Namespace = env.Namespace
370
371
372
		return nil
	}

373
	return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address", envName, env.Server)
374
375
376
377
378
379
380
381
}

// 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) {
382
383
384
385
386
	expander, err := newExpander(cmd)
	if err != nil {
		return nil, err
	}

387
388
389
390
391
392
	//
	// 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.
	//

393
394
	envPresent := envSpec.env != nil
	filesPresent := len(envSpec.files) > 0
395

396
397
	if !envPresent && !filesPresent {
		return nil, fmt.Errorf("Must specify either an environment or a file list, or both")
398
399
	}

400
	fileNames := envSpec.files
401
	if envPresent {
402
		manager, err := metadata.Find(cwd)
403
404
405
406
		if err != nil {
			return nil, err
		}

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

410
411
412
413
414
415
416
417
418
		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...)

419
		if !filesPresent {
420

421
			fileNames = []string{string(envComponentPath)}
422
423
424
425
426
427
428
429
		}
	}

	//
	// Expand templates.
	//

	return expander.Expand(fileNames)
430
}
431
432
433
434
435
436

// constructBaseObj constructs the base Jsonnet object that represents k-v
// pairs of component name -> component imports. For example,
//
//   {
//      foo: import "components/foo.jsonnet"
437
//      "foo-bar": import "components/foo-bar.jsonnet"
438
439
440
441
442
443
444
445
446
447
448
//   }
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)
449
		name = params.SanitizeComponent(name)
450
451
452
		fmt.Fprintf(&obj, "  %s: import \"%s\",\n", name, p)
	}
	obj.WriteString("}\n")
453
	return fmt.Sprintf("%s=%s", metadata.ComponentsExtCodeKey, obj.String())
454
}
455
456
457
458

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