cluster/tools/kartongips: init

This forks bitnami/kubecfg into kartongips. The rationale is that we
want to implement hscloud-specific functionality that wouldn't really be
upstreamable into kubecfg (like secret support, mulit-cluster support).

We forked off from github.com/q3k/kubecfg at commit b6817a94492c561ed61a44eeea2d92dcf2e6b8c0.

Change-Id: If5ba513905e0a86f971576fe7061a471c1d8b398
diff --git a/cluster/tools/kartongips/cmd/root.go b/cluster/tools/kartongips/cmd/root.go
new file mode 100644
index 0000000..b979907
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/root.go
@@ -0,0 +1,423 @@
+// 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.
+
+package cmd
+
+import (
+	"bytes"
+	"encoding/json"
+	goflag "flag"
+	"fmt"
+	"io"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/genuinetools/reg/registry"
+
+	jsonnet "github.com/google/go-jsonnet"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"golang.org/x/crypto/ssh/terminal"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/klog"
+
+	"code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils"
+
+	// Register auth plugins
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
+)
+
+const (
+	flagVerbose     = "verbose"
+	flagJpath       = "jpath"
+	flagJUrl        = "jurl"
+	flagExtVar      = "ext-str"
+	flagExtVarFile  = "ext-str-file"
+	flagExtCode     = "ext-code"
+	flagExtCodeFile = "ext-code-file"
+	flagTLAVar      = "tla-str"
+	flagTLAVarFile  = "tla-str-file"
+	flagTLACode     = "tla-code"
+	flagTLACodeFile = "tla-code-file"
+	flagResolver    = "resolve-images"
+	flagResolvFail  = "resolve-images-error"
+)
+
+var clientConfig clientcmd.ClientConfig
+var overrides clientcmd.ConfigOverrides
+
+func init() {
+	RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
+	RootCmd.PersistentFlags().StringArrayP(flagJpath, "J", nil, "Additional Jsonnet library search path, appended to the ones in the KUBECFG_JPATH env var. May be repeated.")
+	RootCmd.MarkPersistentFlagFilename(flagJpath)
+	RootCmd.PersistentFlags().StringArrayP(flagJUrl, "U", nil, "Additional Jsonnet library search path given as a URL. May be repeated.")
+	RootCmd.PersistentFlags().StringArrayP(flagExtVar, "V", nil, "Values of external variables with string values")
+	RootCmd.PersistentFlags().StringArray(flagExtVarFile, nil, "Read external variables with string values from files")
+	RootCmd.MarkPersistentFlagFilename(flagExtVarFile)
+	RootCmd.PersistentFlags().StringArray(flagExtCode, nil, "Values of external variables with values supplied as Jsonnet code")
+	RootCmd.PersistentFlags().StringArray(flagExtCodeFile, nil, "Read external variables with values supplied as Jsonnet code from files")
+	RootCmd.MarkPersistentFlagFilename(flagExtCodeFile)
+	RootCmd.PersistentFlags().StringArrayP(flagTLAVar, "A", nil, "Values of top level arguments with string values")
+	RootCmd.PersistentFlags().StringArray(flagTLAVarFile, nil, "Read top level arguments with string values from files")
+	RootCmd.MarkPersistentFlagFilename(flagTLAVarFile)
+	RootCmd.PersistentFlags().StringArray(flagTLACode, nil, "Values of top level arguments with values supplied as Jsonnet code")
+	RootCmd.PersistentFlags().StringArray(flagTLACodeFile, nil, "Read top level arguments with values supplied as Jsonnet code from files")
+	RootCmd.MarkPersistentFlagFilename(flagTLACodeFile)
+	RootCmd.PersistentFlags().String(flagResolver, "noop", "Change implementation of resolveImage native function. One of: noop, registry")
+	RootCmd.PersistentFlags().String(flagResolvFail, "warn", "Action when resolveImage fails. One of ignore,warn,error")
+
+	// The "usual" clientcmd/kubectl flags
+	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
+	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
+	kflags := clientcmd.RecommendedConfigOverrideFlags("")
+	RootCmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster")
+	RootCmd.MarkPersistentFlagFilename("kubeconfig")
+	clientcmd.BindOverrideFlags(&overrides, RootCmd.PersistentFlags(), kflags)
+	clientConfig = clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)
+}
+
+// RootCmd is the root of cobra subcommand tree
+var RootCmd = &cobra.Command{
+	Use:           "kubecfg",
+	Short:         "Synchronise Kubernetes resources with config files",
+	SilenceErrors: true,
+	SilenceUsage:  true,
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		goflag.CommandLine.Parse([]string{})
+		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))
+
+		// Ask me how much I love glog/klog's interface.
+		logflags := goflag.NewFlagSet(os.Args[0], goflag.ExitOnError)
+		klog.InitFlags(logflags)
+		logflags.Set("logtostderr", "true")
+		if verbosity >= 2 {
+			// Semi-arbitrary mapping to klog level.
+			logflags.Set("v", fmt.Sprintf("%d", verbosity*3))
+		}
+
+		return nil
+	},
+}
+
+// clientConfig.Namespace() is broken in client-go 3.0:
+// namespace in config erroneously overrides explicit --namespace
+func defaultNamespace(c clientcmd.ClientConfig) (string, error) {
+	if overrides.Context.Namespace != "" {
+		return overrides.Context.Namespace, nil
+	}
+	ns, _, err := c.Namespace()
+	return ns, err
+}
+
+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
+}
+
+// NB: `path` is assumed to be in native-OS path separator form
+func dirURL(path string) *url.URL {
+	path = filepath.ToSlash(path)
+	if path[len(path)-1] != '/' {
+		// trailing slash is important
+		path = path + "/"
+	}
+	return &url.URL{Scheme: "file", Path: path}
+}
+
+// JsonnetVM constructs a new jsonnet.VM, according to command line
+// flags
+func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) {
+	vm := jsonnet.MakeVM()
+	flags := cmd.Flags()
+
+	var searchUrls []*url.URL
+
+	jpath := filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
+
+	jpathArgs, err := flags.GetStringArray(flagJpath)
+	if err != nil {
+		return nil, err
+	}
+	jpath = append(jpath, jpathArgs...)
+
+	for _, p := range jpath {
+		p, err := filepath.Abs(p)
+		if err != nil {
+			return nil, err
+		}
+		searchUrls = append(searchUrls, dirURL(p))
+	}
+
+	sURLs, err := flags.GetStringArray(flagJUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	// Special URL scheme used to find embedded content
+	sURLs = append(sURLs, "internal:///")
+
+	for _, ustr := range sURLs {
+		u, err := url.Parse(ustr)
+		if err != nil {
+			return nil, err
+		}
+		if u.Path[len(u.Path)-1] != '/' {
+			u.Path = u.Path + "/"
+		}
+		searchUrls = append(searchUrls, u)
+	}
+
+	for _, u := range searchUrls {
+		log.Debugln("Jsonnet search path:", u)
+	}
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		return nil, fmt.Errorf("Unable to determine current working directory: %v", err)
+	}
+
+	vm.Importer(utils.MakeUniversalImporter(searchUrls))
+
+	for _, spec := range []struct {
+		flagName string
+		inject   func(string, string)
+		isCode   bool
+		fromFile bool
+	}{
+		{flagExtVar, vm.ExtVar, false, false},
+		// Treat as code to evaluate "importstr":
+		{flagExtVarFile, vm.ExtCode, false, true},
+		{flagExtCode, vm.ExtCode, true, false},
+		{flagExtCodeFile, vm.ExtCode, true, true},
+		{flagTLAVar, vm.TLAVar, false, false},
+		// Treat as code to evaluate "importstr":
+		{flagTLAVarFile, vm.TLACode, false, true},
+		{flagTLACode, vm.TLACode, true, false},
+		{flagTLACodeFile, vm.TLACode, true, true},
+	} {
+		entries, err := flags.GetStringArray(spec.flagName)
+		if err != nil {
+			return nil, err
+		}
+		for _, entry := range entries {
+			kv := strings.SplitN(entry, "=", 2)
+			if spec.fromFile {
+				if len(kv) != 2 {
+					return nil, fmt.Errorf("Failed to parse %s: missing '=' in %s", spec.flagName, entry)
+				}
+				// Ensure that the import path we construct here is absolute, so that our Importer
+				// won't try to glean from an extVar or TLA reference the context necessary to
+				// resolve a relative path.
+				path := kv[1]
+				if !filepath.IsAbs(path) {
+					path = filepath.Join(cwd, path)
+				}
+				u := &url.URL{Scheme: "file", Path: path}
+				var imp string
+				if spec.isCode {
+					imp = "import"
+				} else {
+					imp = "importstr"
+				}
+				spec.inject(kv[0], fmt.Sprintf("%s @'%s'", imp, strings.ReplaceAll(u.String(), "'", "''")))
+			} else {
+				switch len(kv) {
+				case 1:
+					if v, present := os.LookupEnv(kv[0]); present {
+						spec.inject(kv[0], v)
+					} else {
+						return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
+					}
+				case 2:
+					spec.inject(kv[0], kv[1])
+				}
+			}
+		}
+	}
+
+	resolver, err := buildResolver(cmd)
+	if err != nil {
+		return nil, err
+	}
+	utils.RegisterNativeFuncs(vm, resolver)
+
+	return vm, nil
+}
+
+func buildResolver(cmd *cobra.Command) (utils.Resolver, error) {
+	flags := cmd.Flags()
+	resolver, err := flags.GetString(flagResolver)
+	if err != nil {
+		return nil, err
+	}
+	failAction, err := flags.GetString(flagResolvFail)
+	if err != nil {
+		return nil, err
+	}
+
+	ret := resolverErrorWrapper{}
+
+	switch failAction {
+	case "ignore":
+		ret.OnErr = func(error) error { return nil }
+	case "warn":
+		ret.OnErr = func(err error) error {
+			log.Warning(err.Error())
+			return nil
+		}
+	case "error":
+		ret.OnErr = func(err error) error { return err }
+	default:
+		return nil, fmt.Errorf("Bad value for --%s: %s", flagResolvFail, failAction)
+	}
+
+	switch resolver {
+	case "noop":
+		ret.Inner = utils.NewIdentityResolver()
+	case "registry":
+		ret.Inner = utils.NewRegistryResolver(registry.Opt{})
+	default:
+		return nil, fmt.Errorf("Bad value for --%s: %s", flagResolver, resolver)
+	}
+
+	return &ret, nil
+}
+
+type resolverErrorWrapper struct {
+	Inner utils.Resolver
+	OnErr func(error) error
+}
+
+func (r *resolverErrorWrapper) Resolve(image *utils.ImageName) error {
+	err := r.Inner.Resolve(image)
+	if err != nil {
+		err = r.OnErr(err)
+	}
+	return err
+}
+
+func readObjs(cmd *cobra.Command, paths []string) ([]*unstructured.Unstructured, error) {
+	vm, err := JsonnetVM(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	res := []*unstructured.Unstructured{}
+	for _, path := range paths {
+		objs, err := utils.Read(vm, path)
+		if err != nil {
+			return nil, fmt.Errorf("Error reading %s: %v", path, err)
+		}
+		res = append(res, utils.FlattenToV1(objs)...)
+	}
+	return res, nil
+}
+
+// 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())
+}
+
+func getDynamicClients(cmd *cobra.Command) (dynamic.Interface, meta.RESTMapper, discovery.DiscoveryInterface, error) {
+	conf, err := clientConfig.ClientConfig()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("Unable to read kubectl config: %v", err)
+	}
+
+	disco, err := discovery.NewDiscoveryClientForConfig(conf)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	discoCache := utils.NewMemcachedDiscoveryClient(disco)
+
+	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoCache)
+
+	cl, err := dynamic.NewForConfig(conf)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	return cl, mapper, discoCache, nil
+}