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/BUILD.bazel b/cluster/tools/kartongips/cmd/BUILD.bazel
new file mode 100644
index 0000000..dee1b41
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/BUILD.bazel
@@ -0,0 +1,50 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "completion.go",
+ "delete.go",
+ "diff.go",
+ "root.go",
+ "show.go",
+ "update.go",
+ "validate.go",
+ "version.go",
+ ],
+ importpath = "code.hackerspace.pl/hscloud/cluster/tools/kartongips/cmd",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//cluster/tools/kartongips/pkg/kubecfg:go_default_library",
+ "//cluster/tools/kartongips/utils:go_default_library",
+ "@com_github_genuinetools_reg//registry:go_default_library",
+ "@com_github_google_go_jsonnet//:go_default_library",
+ "@com_github_sirupsen_logrus//:go_default_library",
+ "@com_github_spf13_cobra//:go_default_library",
+ "@io_k8s_apimachinery//pkg/api/meta:go_default_library",
+ "@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured:go_default_library",
+ "@io_k8s_client_go//discovery:go_default_library",
+ "@io_k8s_client_go//dynamic:go_default_library",
+ "@io_k8s_client_go//pkg/version:go_default_library",
+ "@io_k8s_client_go//plugin/pkg/client/auth:go_default_library",
+ "@io_k8s_client_go//restmapper:go_default_library",
+ "@io_k8s_client_go//tools/clientcmd:go_default_library",
+ "@io_k8s_klog//:go_default_library",
+ "@org_golang_x_crypto//ssh/terminal:go_default_library",
+ ],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = [
+ "completion_test.go",
+ "show_test.go",
+ "version_test.go",
+ ],
+ embed = [":go_default_library"],
+ deps = [
+ "@com_github_spf13_cobra//:go_default_library",
+ "@com_github_spf13_pflag//:go_default_library",
+ "@in_gopkg_yaml_v2//:go_default_library",
+ ],
+)
diff --git a/cluster/tools/kartongips/cmd/completion.go b/cluster/tools/kartongips/cmd/completion.go
new file mode 100644
index 0000000..eb0f3ce
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/completion.go
@@ -0,0 +1,75 @@
+// Copyright 2018 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 (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "unicode"
+
+ "github.com/spf13/cobra"
+)
+
+const (
+ flagShell = "shell"
+)
+
+func guessShell(path string) string {
+ ret := filepath.Base(path)
+ ret = strings.TrimRightFunc(ret, unicode.IsNumber)
+ return ret
+}
+
+func init() {
+ RootCmd.AddCommand(completionCmd)
+ completionCmd.PersistentFlags().String(flagShell, "", "Shell variant for which to generate completions. Supported values are bash,zsh")
+}
+
+var completionCmd = &cobra.Command{
+ Use: "completion",
+ Short: "Generate shell completions for kubecfg",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ flags := cmd.Flags()
+
+ shell, err := flags.GetString(flagShell)
+ if err != nil {
+ return err
+ }
+ if shell == "" {
+ shell = guessShell(os.Getenv("SHELL"))
+ }
+
+ out := cmd.OutOrStdout()
+
+ switch shell {
+ case "bash":
+ if err := RootCmd.GenBashCompletion(out); err != nil {
+ return err
+ }
+ case "zsh":
+ if err := RootCmd.GenZshCompletion(out); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("Unknown shell %q, try --%s", shell, flagShell)
+ }
+
+ return nil
+ },
+}
diff --git a/cluster/tools/kartongips/cmd/completion_test.go b/cluster/tools/kartongips/cmd/completion_test.go
new file mode 100644
index 0000000..1334aa2
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/completion_test.go
@@ -0,0 +1,19 @@
+package cmd
+
+import (
+ "testing"
+)
+
+func TestGuessShell(t *testing.T) {
+ t.Parallel()
+
+ for _, test := range [][]string{
+ {"/bin/bash", "bash"},
+ {"/usr/bin/zsh", "zsh"},
+ {"/usr/bin/zsh5", "zsh"},
+ } {
+ if result := guessShell(test[0]); result != test[1] {
+ t.Errorf("Guessed %q instead of %q from %q", result, test[1], test[0])
+ }
+ }
+}
diff --git a/cluster/tools/kartongips/cmd/delete.go b/cluster/tools/kartongips/cmd/delete.go
new file mode 100644
index 0000000..5cca212
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/delete.go
@@ -0,0 +1,65 @@
+// 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 (
+ "github.com/spf13/cobra"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/pkg/kubecfg"
+)
+
+const (
+ flagGracePeriod = "grace-period"
+)
+
+func init() {
+ RootCmd.AddCommand(deleteCmd)
+ deleteCmd.PersistentFlags().Int64(flagGracePeriod, -1, "Number of seconds given to resources to terminate gracefully. A negative value is ignored")
+}
+
+var deleteCmd = &cobra.Command{
+ Use: "delete",
+ Short: "Delete Kubernetes resources described in local config",
+ Args: cobra.ArbitraryArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ flags := cmd.Flags()
+ var err error
+
+ c := kubecfg.DeleteCmd{}
+
+ c.GracePeriod, err = flags.GetInt64(flagGracePeriod)
+ if err != nil {
+ return err
+ }
+
+ c.Client, c.Mapper, c.Discovery, err = getDynamicClients(cmd)
+ if err != nil {
+ return err
+ }
+
+ c.DefaultNamespace, err = defaultNamespace(clientConfig)
+ if err != nil {
+ return err
+ }
+
+ objs, err := readObjs(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ return c.Run(cmd.Context(), objs)
+ },
+}
diff --git a/cluster/tools/kartongips/cmd/diff.go b/cluster/tools/kartongips/cmd/diff.go
new file mode 100644
index 0000000..47d92ab
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/diff.go
@@ -0,0 +1,72 @@
+// 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 (
+ "github.com/spf13/cobra"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/pkg/kubecfg"
+)
+
+const (
+ flagDiffStrategy = "diff-strategy"
+ flagOmitSecrets = "omit-secrets"
+)
+
+func init() {
+ diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
+ diffCmd.PersistentFlags().Bool(flagOmitSecrets, false, "hide secret details when showing diff")
+ RootCmd.AddCommand(diffCmd)
+}
+
+var diffCmd = &cobra.Command{
+ Use: "diff",
+ Short: "Display differences between server and local config",
+ Args: cobra.ArbitraryArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ flags := cmd.Flags()
+ var err error
+
+ c := kubecfg.DiffCmd{}
+
+ c.DiffStrategy, err = flags.GetString(flagDiffStrategy)
+ if err != nil {
+ return err
+ }
+
+ c.OmitSecrets, err = flags.GetBool(flagOmitSecrets)
+ if err != nil {
+ return err
+ }
+
+ c.Client, c.Mapper, _, err = getDynamicClients(cmd)
+ if err != nil {
+ return err
+ }
+
+ c.DefaultNamespace, err = defaultNamespace(clientConfig)
+ if err != nil {
+ return err
+ }
+
+ objs, err := readObjs(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ return c.Run(cmd.Context(), objs, cmd.OutOrStdout())
+ },
+}
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
+}
diff --git a/cluster/tools/kartongips/cmd/show.go b/cluster/tools/kartongips/cmd/show.go
new file mode 100644
index 0000000..28ce572
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/show.go
@@ -0,0 +1,55 @@
+// 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 (
+ "github.com/spf13/cobra"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/pkg/kubecfg"
+)
+
+const (
+ flagFormat = "format"
+)
+
+func init() {
+ RootCmd.AddCommand(showCmd)
+ showCmd.PersistentFlags().StringP(flagFormat, "o", "yaml", "Output format. Supported values are: json, yaml")
+}
+
+var showCmd = &cobra.Command{
+ Use: "show",
+ Short: "Show expanded resource definitions",
+ Args: cobra.ArbitraryArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ flags := cmd.Flags()
+ var err error
+
+ c := kubecfg.ShowCmd{}
+
+ c.Format, err = flags.GetString(flagFormat)
+ if err != nil {
+ return err
+ }
+
+ objs, err := readObjs(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ return c.Run(objs, cmd.OutOrStdout())
+ },
+}
diff --git a/cluster/tools/kartongips/cmd/show_test.go b/cluster/tools/kartongips/cmd/show_test.go
new file mode 100644
index 0000000..ab6d60c
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/show_test.go
@@ -0,0 +1,164 @@
+// 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"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+ "gopkg.in/yaml.v2"
+)
+
+func resetFlagsOf(cmd *cobra.Command) {
+ cmd.Flags().VisitAll(func(f *pflag.Flag) {
+ if sv, ok := f.Value.(pflag.SliceValue); ok {
+ sv.Replace(nil)
+ } else {
+ f.Value.Set(f.DefValue)
+ }
+ })
+}
+
+func cmdOutput(t *testing.T, args []string) string {
+ var buf bytes.Buffer
+ RootCmd.SetOutput(&buf)
+ defer RootCmd.SetOutput(nil)
+
+ t.Log("Running args", args)
+ RootCmd.SetArgs(args)
+ if err := RootCmd.Execute(); err != nil {
+ t.Fatal("command failed:", err)
+ }
+
+ return buf.String()
+}
+
+func TestShow(t *testing.T) {
+ formats := map[string]func(string) (interface{}, error){
+ "json": func(text string) (ret interface{}, err error) {
+ err = json.Unmarshal([]byte(text), &ret)
+ return
+ },
+ "yaml": func(text string) (ret interface{}, err error) {
+ err = yaml.Unmarshal([]byte(text), &ret)
+ return
+ },
+ }
+
+ // Use the fact that JSON is also valid YAML ..
+ expected := `
+{
+ "apiVersion": "v0alpha1",
+ "kind": "TestObject",
+ "nil": null,
+ "bool": true,
+ "number": 42,
+ "string": "bar",
+ "notAVal": "aVal",
+ "notAnotherVal": "aVal2",
+ "filevar": "foo\n",
+ "array": ["one", 2, [3]],
+ "object": {"foo": "bar"},
+ "extcode": {"foo": 1, "bar": "test"}
+}
+`
+
+ for format, parser := range formats {
+ expected, err := parser(expected)
+ if err != nil {
+ t.Fatalf("error parsing *expected* value: %v", err)
+ }
+
+ os.Setenv("anVar", "aVal2")
+ defer os.Unsetenv("anVar")
+
+ output := cmdOutput(t, []string{"show",
+ "-J", filepath.FromSlash("../testdata/lib"),
+ "-o", format,
+ filepath.FromSlash("../testdata/test.jsonnet"),
+ "-V", "aVar=aVal",
+ "-V", "anVar",
+ "--ext-str-file", "filevar=" + filepath.FromSlash("../testdata/extvar.file"),
+ "--ext-code", `extcode={foo: 1, bar: "test"}`,
+ })
+ defer resetFlagsOf(RootCmd)
+
+ t.Log("output is", output)
+ actual, err := parser(output)
+ if err != nil {
+ t.Errorf("error parsing output of format %s: %v", format, err)
+ } else if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("format %s expected != actual: %s != %s", format, expected, actual)
+ }
+ }
+}
+
+func TestShowUsingExtVarFiles(t *testing.T) {
+ expectedText := `
+{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": {
+ "name": "sink"
+ },
+ "data": {
+ "input": {
+ "greeting": "Hello!",
+ "helper": true,
+ "top": true
+ },
+ "var": "I'm a var!"
+ }
+}
+`
+ var expected interface{}
+ if err := json.Unmarshal([]byte(expectedText), &expected); err != nil {
+ t.Fatalf("error parsing *expected* value: %v", err)
+ }
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("failed to get current working directory: %v", err)
+ }
+ if err := os.Chdir("../testdata/extvars/feed"); err != nil {
+ t.Fatalf("failed to change to target directory: %v", err)
+ }
+ defer os.Chdir(cwd)
+
+ output := cmdOutput(t, []string{"show",
+ "top.jsonnet",
+ "-o", "json",
+ "--tla-code-file", "input=input.jsonnet",
+ "--tla-code-file", "sink=sink.jsonnet",
+ "--ext-str-file", "filevar=var.txt",
+ })
+ defer resetFlagsOf(RootCmd)
+
+ t.Log("output is", output)
+ var actual interface{}
+ err = json.Unmarshal([]byte(output), &actual)
+ if err != nil {
+ t.Errorf("error parsing output: %v", err)
+ } else if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("expected != actual: %s != %s", expected, actual)
+ }
+}
diff --git a/cluster/tools/kartongips/cmd/update.go b/cluster/tools/kartongips/cmd/update.go
new file mode 100644
index 0000000..7a741a6
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/update.go
@@ -0,0 +1,109 @@
+// 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 (
+ "github.com/spf13/cobra"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/pkg/kubecfg"
+)
+
+const (
+ flagCreate = "create"
+ flagSkipGc = "skip-gc"
+ flagGcTag = "gc-tag"
+ flagDryRun = "dry-run"
+ flagValidate = "validate"
+)
+
+func init() {
+ RootCmd.AddCommand(updateCmd)
+ updateCmd.PersistentFlags().Bool(flagCreate, true, "Create missing resources")
+ updateCmd.PersistentFlags().Bool(flagSkipGc, false, "Don't perform garbage collection, even with --"+flagGcTag)
+ updateCmd.PersistentFlags().String(flagGcTag, "", "Add this tag to updated objects, and garbage collect existing objects with this tag and not in config")
+ updateCmd.PersistentFlags().Bool(flagDryRun, false, "Perform only read-only operations")
+ updateCmd.PersistentFlags().Bool(flagValidate, true, "Validate input against server schema")
+ updateCmd.PersistentFlags().Bool(flagIgnoreUnknown, false, "Don't fail validation if the schema for a given resource type is not found")
+}
+
+var updateCmd = &cobra.Command{
+ Use: "update",
+ Short: "Update Kubernetes resources with local config",
+ Args: cobra.ArbitraryArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ flags := cmd.Flags()
+ var err error
+ c := kubecfg.UpdateCmd{}
+
+ validate, err := flags.GetBool(flagValidate)
+ if err != nil {
+ return err
+ }
+
+ c.Create, err = flags.GetBool(flagCreate)
+ if err != nil {
+ return err
+ }
+
+ c.GcTag, err = flags.GetString(flagGcTag)
+ if err != nil {
+ return err
+ }
+
+ c.SkipGc, err = flags.GetBool(flagSkipGc)
+ if err != nil {
+ return err
+ }
+
+ c.DryRun, err = flags.GetBool(flagDryRun)
+ if err != nil {
+ return err
+ }
+
+ c.Client, c.Mapper, c.Discovery, err = getDynamicClients(cmd)
+ if err != nil {
+ return err
+ }
+
+ c.DefaultNamespace, err = defaultNamespace(clientConfig)
+ if err != nil {
+ return err
+ }
+
+ objs, err := readObjs(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ if validate {
+ v := kubecfg.ValidateCmd{
+ Mapper: c.Mapper,
+ Discovery: c.Discovery,
+ }
+
+ v.IgnoreUnknown, err = flags.GetBool(flagIgnoreUnknown)
+ if err != nil {
+ return err
+ }
+
+ if err := v.Run(objs, cmd.OutOrStdout()); err != nil {
+ return err
+ }
+ }
+
+ return c.Run(cmd.Context(), objs)
+ },
+}
diff --git a/cluster/tools/kartongips/cmd/validate.go b/cluster/tools/kartongips/cmd/validate.go
new file mode 100644
index 0000000..d68bbd9
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/validate.go
@@ -0,0 +1,60 @@
+// 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 (
+ "github.com/spf13/cobra"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/pkg/kubecfg"
+)
+
+const (
+ flagIgnoreUnknown = "ignore-unknown"
+)
+
+func init() {
+ RootCmd.AddCommand(validateCmd)
+ validateCmd.PersistentFlags().Bool(flagIgnoreUnknown, true, "Don't fail if the schema for a given resource type is not found")
+}
+
+var validateCmd = &cobra.Command{
+ Use: "validate",
+ Short: "Compare generated manifest against server OpenAPI spec",
+ Args: cobra.ArbitraryArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ flags := cmd.Flags()
+ var err error
+
+ c := kubecfg.ValidateCmd{}
+
+ _, c.Mapper, c.Discovery, err = getDynamicClients(cmd)
+ if err != nil {
+ return err
+ }
+
+ c.IgnoreUnknown, err = flags.GetBool(flagIgnoreUnknown)
+ if err != nil {
+ return err
+ }
+
+ objs, err := readObjs(cmd, args)
+ if err != nil {
+ return err
+ }
+
+ return c.Run(objs, cmd.OutOrStdout())
+ },
+}
diff --git a/cluster/tools/kartongips/cmd/version.go b/cluster/tools/kartongips/cmd/version.go
new file mode 100644
index 0000000..fb9b7eb
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/version.go
@@ -0,0 +1,43 @@
+// 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 (
+ "fmt"
+
+ jsonnet "github.com/google/go-jsonnet"
+ "github.com/spf13/cobra"
+ "k8s.io/client-go/pkg/version"
+)
+
+func init() {
+ RootCmd.AddCommand(versionCmd)
+}
+
+// Version is overridden by main
+var Version = "(dev build)"
+
+var versionCmd = &cobra.Command{
+ Use: "version",
+ Short: "Print version information",
+ Args: cobra.NoArgs,
+ Run: func(cmd *cobra.Command, args []string) {
+ out := cmd.OutOrStdout()
+ fmt.Fprintln(out, "kubecfg version:", Version)
+ fmt.Fprintln(out, "jsonnet version:", jsonnet.Version())
+ fmt.Fprintln(out, "client-go version:", version.Get())
+ },
+}
diff --git a/cluster/tools/kartongips/cmd/version_test.go b/cluster/tools/kartongips/cmd/version_test.go
new file mode 100644
index 0000000..68f6208
--- /dev/null
+++ b/cluster/tools/kartongips/cmd/version_test.go
@@ -0,0 +1,30 @@
+// 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 (
+ "regexp"
+ "testing"
+)
+
+func TestVersion(t *testing.T) {
+ output := cmdOutput(t, []string{"version"})
+
+ // Also a good smoke-test that libjsonnet linked successfully
+ if !regexp.MustCompile(`jsonnet version: v[\d.]+`).MatchString(output) {
+ t.Error("Failed to find jsonnet version in:", output)
+ }
+}