blob: b97990751eaa8b8a350b6915a6334a0e292984d6 [file] [log] [blame]
Serge Bazanskibe538db2020-11-12 00:22:42 +01001// Copyright 2017 The kubecfg authors
2//
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16package cmd
17
18import (
19 "bytes"
20 "encoding/json"
21 goflag "flag"
22 "fmt"
23 "io"
24 "net/url"
25 "os"
26 "path/filepath"
27 "strings"
28
29 "github.com/genuinetools/reg/registry"
30
31 jsonnet "github.com/google/go-jsonnet"
32 log "github.com/sirupsen/logrus"
33 "github.com/spf13/cobra"
34 "golang.org/x/crypto/ssh/terminal"
35 "k8s.io/apimachinery/pkg/api/meta"
36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
37 "k8s.io/client-go/discovery"
38 "k8s.io/client-go/dynamic"
39 "k8s.io/client-go/restmapper"
40 "k8s.io/client-go/tools/clientcmd"
41 "k8s.io/klog"
42
43 "code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils"
44
45 // Register auth plugins
46 _ "k8s.io/client-go/plugin/pkg/client/auth"
47)
48
49const (
50 flagVerbose = "verbose"
51 flagJpath = "jpath"
52 flagJUrl = "jurl"
53 flagExtVar = "ext-str"
54 flagExtVarFile = "ext-str-file"
55 flagExtCode = "ext-code"
56 flagExtCodeFile = "ext-code-file"
57 flagTLAVar = "tla-str"
58 flagTLAVarFile = "tla-str-file"
59 flagTLACode = "tla-code"
60 flagTLACodeFile = "tla-code-file"
61 flagResolver = "resolve-images"
62 flagResolvFail = "resolve-images-error"
63)
64
65var clientConfig clientcmd.ClientConfig
66var overrides clientcmd.ConfigOverrides
67
68func init() {
69 RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.")
70 RootCmd.PersistentFlags().StringArrayP(flagJpath, "J", nil, "Additional Jsonnet library search path, appended to the ones in the KUBECFG_JPATH env var. May be repeated.")
71 RootCmd.MarkPersistentFlagFilename(flagJpath)
72 RootCmd.PersistentFlags().StringArrayP(flagJUrl, "U", nil, "Additional Jsonnet library search path given as a URL. May be repeated.")
73 RootCmd.PersistentFlags().StringArrayP(flagExtVar, "V", nil, "Values of external variables with string values")
74 RootCmd.PersistentFlags().StringArray(flagExtVarFile, nil, "Read external variables with string values from files")
75 RootCmd.MarkPersistentFlagFilename(flagExtVarFile)
76 RootCmd.PersistentFlags().StringArray(flagExtCode, nil, "Values of external variables with values supplied as Jsonnet code")
77 RootCmd.PersistentFlags().StringArray(flagExtCodeFile, nil, "Read external variables with values supplied as Jsonnet code from files")
78 RootCmd.MarkPersistentFlagFilename(flagExtCodeFile)
79 RootCmd.PersistentFlags().StringArrayP(flagTLAVar, "A", nil, "Values of top level arguments with string values")
80 RootCmd.PersistentFlags().StringArray(flagTLAVarFile, nil, "Read top level arguments with string values from files")
81 RootCmd.MarkPersistentFlagFilename(flagTLAVarFile)
82 RootCmd.PersistentFlags().StringArray(flagTLACode, nil, "Values of top level arguments with values supplied as Jsonnet code")
83 RootCmd.PersistentFlags().StringArray(flagTLACodeFile, nil, "Read top level arguments with values supplied as Jsonnet code from files")
84 RootCmd.MarkPersistentFlagFilename(flagTLACodeFile)
85 RootCmd.PersistentFlags().String(flagResolver, "noop", "Change implementation of resolveImage native function. One of: noop, registry")
86 RootCmd.PersistentFlags().String(flagResolvFail, "warn", "Action when resolveImage fails. One of ignore,warn,error")
87
88 // The "usual" clientcmd/kubectl flags
89 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
90 loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
91 kflags := clientcmd.RecommendedConfigOverrideFlags("")
92 RootCmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster")
93 RootCmd.MarkPersistentFlagFilename("kubeconfig")
94 clientcmd.BindOverrideFlags(&overrides, RootCmd.PersistentFlags(), kflags)
95 clientConfig = clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)
96}
97
98// RootCmd is the root of cobra subcommand tree
99var RootCmd = &cobra.Command{
100 Use: "kubecfg",
101 Short: "Synchronise Kubernetes resources with config files",
102 SilenceErrors: true,
103 SilenceUsage: true,
104 PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
105 goflag.CommandLine.Parse([]string{})
106 flags := cmd.Flags()
107 out := cmd.OutOrStderr()
108 log.SetOutput(out)
109
110 logFmt := NewLogFormatter(out)
111 log.SetFormatter(logFmt)
112
113 verbosity, err := flags.GetCount(flagVerbose)
114 if err != nil {
115 return err
116 }
117 log.SetLevel(logLevel(verbosity))
118
119 // Ask me how much I love glog/klog's interface.
120 logflags := goflag.NewFlagSet(os.Args[0], goflag.ExitOnError)
121 klog.InitFlags(logflags)
122 logflags.Set("logtostderr", "true")
123 if verbosity >= 2 {
124 // Semi-arbitrary mapping to klog level.
125 logflags.Set("v", fmt.Sprintf("%d", verbosity*3))
126 }
127
128 return nil
129 },
130}
131
132// clientConfig.Namespace() is broken in client-go 3.0:
133// namespace in config erroneously overrides explicit --namespace
134func defaultNamespace(c clientcmd.ClientConfig) (string, error) {
135 if overrides.Context.Namespace != "" {
136 return overrides.Context.Namespace, nil
137 }
138 ns, _, err := c.Namespace()
139 return ns, err
140}
141
142func logLevel(verbosity int) log.Level {
143 switch verbosity {
144 case 0:
145 return log.InfoLevel
146 default:
147 return log.DebugLevel
148 }
149}
150
151type logFormatter struct {
152 escapes *terminal.EscapeCodes
153 colorise bool
154}
155
156// NewLogFormatter creates a new log.Formatter customised for writer
157func NewLogFormatter(out io.Writer) log.Formatter {
158 var ret = logFormatter{}
159 if f, ok := out.(*os.File); ok {
160 ret.colorise = terminal.IsTerminal(int(f.Fd()))
161 ret.escapes = terminal.NewTerminal(f, "").Escape
162 }
163 return &ret
164}
165
166func (f *logFormatter) levelEsc(level log.Level) []byte {
167 switch level {
168 case log.DebugLevel:
169 return []byte{}
170 case log.WarnLevel:
171 return f.escapes.Yellow
172 case log.ErrorLevel, log.FatalLevel, log.PanicLevel:
173 return f.escapes.Red
174 default:
175 return f.escapes.Blue
176 }
177}
178
179func (f *logFormatter) Format(e *log.Entry) ([]byte, error) {
180 buf := bytes.Buffer{}
181 if f.colorise {
182 buf.Write(f.levelEsc(e.Level))
183 fmt.Fprintf(&buf, "%-5s ", strings.ToUpper(e.Level.String()))
184 buf.Write(f.escapes.Reset)
185 }
186
187 buf.WriteString(strings.TrimSpace(e.Message))
188 buf.WriteString("\n")
189
190 return buf.Bytes(), nil
191}
192
193// NB: `path` is assumed to be in native-OS path separator form
194func dirURL(path string) *url.URL {
195 path = filepath.ToSlash(path)
196 if path[len(path)-1] != '/' {
197 // trailing slash is important
198 path = path + "/"
199 }
200 return &url.URL{Scheme: "file", Path: path}
201}
202
203// JsonnetVM constructs a new jsonnet.VM, according to command line
204// flags
205func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) {
206 vm := jsonnet.MakeVM()
207 flags := cmd.Flags()
208
209 var searchUrls []*url.URL
210
211 jpath := filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
212
213 jpathArgs, err := flags.GetStringArray(flagJpath)
214 if err != nil {
215 return nil, err
216 }
217 jpath = append(jpath, jpathArgs...)
218
219 for _, p := range jpath {
220 p, err := filepath.Abs(p)
221 if err != nil {
222 return nil, err
223 }
224 searchUrls = append(searchUrls, dirURL(p))
225 }
226
227 sURLs, err := flags.GetStringArray(flagJUrl)
228 if err != nil {
229 return nil, err
230 }
231
232 // Special URL scheme used to find embedded content
233 sURLs = append(sURLs, "internal:///")
234
235 for _, ustr := range sURLs {
236 u, err := url.Parse(ustr)
237 if err != nil {
238 return nil, err
239 }
240 if u.Path[len(u.Path)-1] != '/' {
241 u.Path = u.Path + "/"
242 }
243 searchUrls = append(searchUrls, u)
244 }
245
246 for _, u := range searchUrls {
247 log.Debugln("Jsonnet search path:", u)
248 }
249
250 cwd, err := os.Getwd()
251 if err != nil {
252 return nil, fmt.Errorf("Unable to determine current working directory: %v", err)
253 }
254
255 vm.Importer(utils.MakeUniversalImporter(searchUrls))
256
257 for _, spec := range []struct {
258 flagName string
259 inject func(string, string)
260 isCode bool
261 fromFile bool
262 }{
263 {flagExtVar, vm.ExtVar, false, false},
264 // Treat as code to evaluate "importstr":
265 {flagExtVarFile, vm.ExtCode, false, true},
266 {flagExtCode, vm.ExtCode, true, false},
267 {flagExtCodeFile, vm.ExtCode, true, true},
268 {flagTLAVar, vm.TLAVar, false, false},
269 // Treat as code to evaluate "importstr":
270 {flagTLAVarFile, vm.TLACode, false, true},
271 {flagTLACode, vm.TLACode, true, false},
272 {flagTLACodeFile, vm.TLACode, true, true},
273 } {
274 entries, err := flags.GetStringArray(spec.flagName)
275 if err != nil {
276 return nil, err
277 }
278 for _, entry := range entries {
279 kv := strings.SplitN(entry, "=", 2)
280 if spec.fromFile {
281 if len(kv) != 2 {
282 return nil, fmt.Errorf("Failed to parse %s: missing '=' in %s", spec.flagName, entry)
283 }
284 // Ensure that the import path we construct here is absolute, so that our Importer
285 // won't try to glean from an extVar or TLA reference the context necessary to
286 // resolve a relative path.
287 path := kv[1]
288 if !filepath.IsAbs(path) {
289 path = filepath.Join(cwd, path)
290 }
291 u := &url.URL{Scheme: "file", Path: path}
292 var imp string
293 if spec.isCode {
294 imp = "import"
295 } else {
296 imp = "importstr"
297 }
298 spec.inject(kv[0], fmt.Sprintf("%s @'%s'", imp, strings.ReplaceAll(u.String(), "'", "''")))
299 } else {
300 switch len(kv) {
301 case 1:
302 if v, present := os.LookupEnv(kv[0]); present {
303 spec.inject(kv[0], v)
304 } else {
305 return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
306 }
307 case 2:
308 spec.inject(kv[0], kv[1])
309 }
310 }
311 }
312 }
313
314 resolver, err := buildResolver(cmd)
315 if err != nil {
316 return nil, err
317 }
318 utils.RegisterNativeFuncs(vm, resolver)
319
320 return vm, nil
321}
322
323func buildResolver(cmd *cobra.Command) (utils.Resolver, error) {
324 flags := cmd.Flags()
325 resolver, err := flags.GetString(flagResolver)
326 if err != nil {
327 return nil, err
328 }
329 failAction, err := flags.GetString(flagResolvFail)
330 if err != nil {
331 return nil, err
332 }
333
334 ret := resolverErrorWrapper{}
335
336 switch failAction {
337 case "ignore":
338 ret.OnErr = func(error) error { return nil }
339 case "warn":
340 ret.OnErr = func(err error) error {
341 log.Warning(err.Error())
342 return nil
343 }
344 case "error":
345 ret.OnErr = func(err error) error { return err }
346 default:
347 return nil, fmt.Errorf("Bad value for --%s: %s", flagResolvFail, failAction)
348 }
349
350 switch resolver {
351 case "noop":
352 ret.Inner = utils.NewIdentityResolver()
353 case "registry":
354 ret.Inner = utils.NewRegistryResolver(registry.Opt{})
355 default:
356 return nil, fmt.Errorf("Bad value for --%s: %s", flagResolver, resolver)
357 }
358
359 return &ret, nil
360}
361
362type resolverErrorWrapper struct {
363 Inner utils.Resolver
364 OnErr func(error) error
365}
366
367func (r *resolverErrorWrapper) Resolve(image *utils.ImageName) error {
368 err := r.Inner.Resolve(image)
369 if err != nil {
370 err = r.OnErr(err)
371 }
372 return err
373}
374
375func readObjs(cmd *cobra.Command, paths []string) ([]*unstructured.Unstructured, error) {
376 vm, err := JsonnetVM(cmd)
377 if err != nil {
378 return nil, err
379 }
380
381 res := []*unstructured.Unstructured{}
382 for _, path := range paths {
383 objs, err := utils.Read(vm, path)
384 if err != nil {
385 return nil, fmt.Errorf("Error reading %s: %v", path, err)
386 }
387 res = append(res, utils.FlattenToV1(objs)...)
388 }
389 return res, nil
390}
391
392// For debugging
393func dumpJSON(v interface{}) string {
394 buf := bytes.NewBuffer(nil)
395 enc := json.NewEncoder(buf)
396 enc.SetIndent("", " ")
397 if err := enc.Encode(v); err != nil {
398 return err.Error()
399 }
400 return string(buf.Bytes())
401}
402
403func getDynamicClients(cmd *cobra.Command) (dynamic.Interface, meta.RESTMapper, discovery.DiscoveryInterface, error) {
404 conf, err := clientConfig.ClientConfig()
405 if err != nil {
406 return nil, nil, nil, fmt.Errorf("Unable to read kubectl config: %v", err)
407 }
408
409 disco, err := discovery.NewDiscoveryClientForConfig(conf)
410 if err != nil {
411 return nil, nil, nil, err
412 }
413 discoCache := utils.NewMemcachedDiscoveryClient(disco)
414
415 mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoCache)
416
417 cl, err := dynamic.NewForConfig(conf)
418 if err != nil {
419 return nil, nil, nil, err
420 }
421
422 return cl, mapper, discoCache, nil
423}