blob: b97990751eaa8b8a350b6915a6334a0e292984d6 [file] [log] [blame]
// 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
}