github.com/splunk/dan1-qbec@v0.7.3/internal/commands/config.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     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  */
    16  
    17  package commands
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"strings"
    25  
    26  	"github.com/chzyer/readline"
    27  	"github.com/pkg/errors"
    28  	"github.com/splunk/qbec/internal/eval"
    29  	"github.com/splunk/qbec/internal/model"
    30  	"github.com/splunk/qbec/internal/objsort"
    31  	"github.com/splunk/qbec/internal/remote"
    32  	"github.com/splunk/qbec/internal/sio"
    33  	"github.com/splunk/qbec/internal/vm"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  )
    36  
    37  // clientProvider returns a client for the supplied environment.
    38  type clientProvider func(env string) (Client, error)
    39  
    40  type kubeAttrsProvider func(env string) (*remote.KubeAttributes, error)
    41  
    42  // stdClientProvider provides clients based on the supplied Kubernetes config
    43  type stdClientProvider struct {
    44  	app       *model.App
    45  	config    *remote.Config
    46  	verbosity int
    47  }
    48  
    49  // Client returns a client for the supplied environment.
    50  func (s stdClientProvider) Client(env string) (Client, error) {
    51  	server, err := s.app.ServerURL(env)
    52  	if err != nil {
    53  		return nil, errors.Wrap(err, "get client")
    54  	}
    55  	ns := s.app.DefaultNamespace(env)
    56  	rem, err := s.config.Client(remote.ConnectOpts{
    57  		EnvName:   env,
    58  		ServerURL: server,
    59  		Namespace: ns,
    60  		Verbosity: s.verbosity,
    61  	})
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	return rem, nil
    66  }
    67  
    68  func (s stdClientProvider) Attrs(env string) (*remote.KubeAttributes, error) {
    69  	server, err := s.app.ServerURL(env)
    70  	if err != nil {
    71  		return nil, errors.Wrap(err, "get kubernetes attrs")
    72  	}
    73  	ns := s.app.DefaultNamespace(env)
    74  	rem, err := s.config.KubeAttributes(remote.ConnectOpts{
    75  		EnvName:   env,
    76  		ServerURL: server,
    77  		Namespace: ns,
    78  		Verbosity: s.verbosity,
    79  	})
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	return rem, nil
    84  }
    85  
    86  // ConfigFactory provides a config.
    87  type ConfigFactory struct {
    88  	Stdout          io.Writer //standard output for command
    89  	Stderr          io.Writer // standard error for command
    90  	SkipConfirm     bool      // do not prompt for confirmation
    91  	Colors          bool      // show colorized output
    92  	EvalConcurrency int       // concurrency of eval operations
    93  	Verbosity       int       // verbosity level
    94  	StrictVars      bool      // strict mode for variable evaluation
    95  }
    96  
    97  func (cp ConfigFactory) internalConfig(app *model.App, vmConfig vm.Config, clp clientProvider, kp kubeAttrsProvider) (*Config, error) {
    98  	var stdout io.Writer = os.Stdout
    99  	var stderr io.Writer = os.Stderr
   100  
   101  	if cp.Stdout != nil {
   102  		stdout = cp.Stdout
   103  	}
   104  	if cp.Stderr != nil {
   105  		stderr = cp.Stderr
   106  	}
   107  
   108  	cfg := &Config{
   109  		app:             app,
   110  		vmc:             vmConfig,
   111  		clp:             clp,
   112  		attrsp:          kp,
   113  		colors:          cp.Colors,
   114  		yes:             cp.SkipConfirm,
   115  		evalConcurrency: cp.EvalConcurrency,
   116  		verbose:         cp.Verbosity,
   117  		stdin:           os.Stdin,
   118  		stdout:          stdout,
   119  		stderr:          stderr,
   120  	}
   121  	if err := cfg.init(cp.StrictVars); err != nil {
   122  		return nil, err
   123  	}
   124  	return cfg, nil
   125  }
   126  
   127  // Config returns the command configuration.
   128  func (cp ConfigFactory) Config(app *model.App, vmConfig vm.Config, remoteConfig *remote.Config) (*Config, error) {
   129  	scp := &stdClientProvider{
   130  		app:       app,
   131  		config:    remoteConfig,
   132  		verbosity: cp.Verbosity,
   133  	}
   134  	return cp.internalConfig(app, vmConfig, scp.Client, scp.Attrs)
   135  }
   136  
   137  // Config is the command configuration.
   138  type Config struct {
   139  	app             *model.App        // app loaded from file
   140  	vmc             vm.Config         // jsonnet VM config
   141  	tlaVars         map[string]string // all top level string vars specified for the command
   142  	tlaCodeVars     map[string]string // all top level code vars specified for the command
   143  	clp             clientProvider    // the client provider
   144  	attrsp          kubeAttrsProvider // the kubernetes attribute provider
   145  	colors          bool              // colorize output
   146  	yes             bool              // auto-confirm
   147  	evalConcurrency int               // concurrency of component eval
   148  	verbose         int               // verbosity level
   149  	stdin           io.Reader         // standard input
   150  	stdout          io.Writer         // standard output
   151  	stderr          io.Writer         // standard error
   152  	cleanEvalMode   bool              // clean mode for eval
   153  }
   154  
   155  // init checks variables and sets up defaults. In strict mode, it requires all variables
   156  // to be specified and does not allow undeclared variables to be passed in.
   157  // It also sets the base VM config to include the library paths from the app definition
   158  // and exclude all TLA variables. Require TLA variables are set per component later.
   159  func (c *Config) init(strict bool) error {
   160  	var msgs []string
   161  	c.tlaVars = c.vmc.TopLevelVars()
   162  	c.tlaCodeVars = c.vmc.TopLevelCodeVars()
   163  	c.vmc = c.vmc.WithLibPaths(c.app.LibPaths())
   164  
   165  	vars := c.vmc.Vars()
   166  	codeVars := c.vmc.CodeVars()
   167  
   168  	declaredExternals := c.app.DeclaredVars()
   169  	declaredTLAs := c.app.DeclaredTopLevelVars()
   170  
   171  	checkStrict := func(tla bool, declared map[string]interface{}, varSources ...map[string]string) {
   172  		kind := "external"
   173  		if tla {
   174  			kind = "top level"
   175  		}
   176  		// check that all specified variables have been declared
   177  		for _, src := range varSources {
   178  			for k := range src {
   179  				_, ok := declared[k]
   180  				if !ok {
   181  					msgs = append(msgs, fmt.Sprintf("specified %s variable '%s' not declared for app", kind, k))
   182  				}
   183  			}
   184  		}
   185  		// check that all declared variables have been specified
   186  		var fn func(string) bool
   187  		if tla {
   188  			fn = c.vmc.HasTopLevelVar
   189  		} else {
   190  			fn = c.vmc.HasVar
   191  		}
   192  		for k := range declared {
   193  			ok := fn(k)
   194  			if !ok {
   195  				msgs = append(msgs, fmt.Sprintf("declared %s variable '%s' not specfied for command", kind, k))
   196  			}
   197  		}
   198  	}
   199  
   200  	if strict {
   201  		checkStrict(false, declaredExternals, vars, codeVars)
   202  		checkStrict(true, declaredTLAs, c.tlaVars, c.tlaCodeVars)
   203  		if len(msgs) > 0 {
   204  			return fmt.Errorf("strict vars check failures\n\t%s", strings.Join(msgs, "\n\t"))
   205  		}
   206  	}
   207  
   208  	// apply default values for external vars
   209  	addStrings, addCodes := map[string]string{}, map[string]string{}
   210  
   211  	for k, v := range declaredExternals {
   212  		if c.vmc.HasVar(k) {
   213  			continue
   214  		}
   215  		if v == nil {
   216  			sio.Warnf("no/ nil default specified for variable %q\n", k)
   217  			continue
   218  		}
   219  		switch t := v.(type) {
   220  		case string:
   221  			addStrings[k] = t
   222  		default:
   223  			b, err := json.Marshal(v)
   224  			if err != nil {
   225  				return fmt.Errorf("json marshal: unexpected error marshaling default for variable %s, %v", k, err)
   226  			}
   227  			addCodes[k] = string(b)
   228  		}
   229  	}
   230  	c.vmc = c.vmc.WithoutTopLevel().WithVars(addStrings).WithCodeVars(addCodes)
   231  	return nil
   232  }
   233  
   234  // App returns the application object loaded for this run.
   235  func (c Config) App() *model.App { return c.app }
   236  
   237  // EvalContext returns the evaluation context for the supplied environment.
   238  func (c Config) EvalContext(env string) eval.Context {
   239  	return eval.Context{
   240  		App:             c.App().Name(),
   241  		Tag:             c.App().Tag(),
   242  		Env:             env,
   243  		DefaultNs:       c.app.DefaultNamespace(env),
   244  		VMConfig:        c.vmConfig,
   245  		Verbose:         c.Verbosity() > 1,
   246  		Concurrency:     c.EvalConcurrency(),
   247  		PostProcessFile: c.App().PostProcessor(),
   248  		CleanMode:       c.cleanEvalMode,
   249  	}
   250  }
   251  
   252  // vmConfig returns the VM configuration that only has the supplied top-level arguments.
   253  func (c Config) vmConfig(tlaVars []string) vm.Config {
   254  	cfg := c.vmc.WithoutTopLevel()
   255  
   256  	// common case to avoid useless object creation. If no required vars
   257  	// needed or none present, just return the config with empty TLAs
   258  	if len(tlaVars) == 0 || (len(c.tlaVars) == 0 && len(c.tlaCodeVars) == 0) {
   259  		return cfg
   260  	}
   261  
   262  	// else create a subset that match requirements
   263  	check := map[string]bool{}
   264  	for _, v := range tlaVars {
   265  		check[v] = true
   266  	}
   267  
   268  	addStrs := map[string]string{}
   269  	for k, v := range c.tlaVars {
   270  		if check[k] {
   271  			addStrs[k] = v
   272  		}
   273  	}
   274  	addCodes := map[string]string{}
   275  	for k, v := range c.tlaCodeVars {
   276  		if check[k] {
   277  			addCodes[k] = v
   278  		}
   279  	}
   280  	return cfg.WithTopLevelVars(addStrs).WithTopLevelCodeVars(addCodes)
   281  }
   282  
   283  // Client returns a client for the supplied environment
   284  func (c Config) Client(env string) (Client, error) {
   285  	return c.clp(env)
   286  }
   287  
   288  // KubeAttributes returns the kubernetes attributes for the supplied environment
   289  func (c Config) KubeAttributes(env string) (*remote.KubeAttributes, error) {
   290  	return c.attrsp(env)
   291  }
   292  
   293  // Colorize returns true if output needs to be colorized.
   294  func (c Config) Colorize() bool { return c.colors }
   295  
   296  // Verbosity returns the log verbosity level
   297  func (c Config) Verbosity() int { return c.verbose }
   298  
   299  // EvalConcurrency returns the concurrency to be used for evaluating components.
   300  func (c Config) EvalConcurrency() int { return c.evalConcurrency }
   301  
   302  // Stdout returns the standard output configured for the command.
   303  func (c Config) Stdout() io.Writer {
   304  	return c.stdout
   305  }
   306  
   307  // Stderr returns the standard error configured for the command.
   308  func (c Config) Stderr() io.Writer {
   309  	return c.stderr
   310  }
   311  
   312  // Confirm prompts for confirmation if needed.
   313  func (c Config) Confirm(context string) error {
   314  	fmt.Fprintln(c.stderr)
   315  	fmt.Fprintln(c.stderr, context)
   316  	fmt.Fprintln(c.stderr)
   317  	if c.yes {
   318  		return nil
   319  	}
   320  	inst, err := readline.NewEx(&readline.Config{
   321  		Prompt: "Do you want to continue [y/n]: ",
   322  		Stdin:  c.stdin,
   323  		Stdout: c.stdout,
   324  		Stderr: c.stderr,
   325  	})
   326  	if err != nil {
   327  		return err
   328  	}
   329  	for {
   330  		s, err := inst.Readline()
   331  		if err != nil {
   332  			return err
   333  		}
   334  		if s == "y" {
   335  			return nil
   336  		}
   337  		if s == "n" {
   338  			return errors.New("canceled")
   339  		}
   340  	}
   341  }
   342  
   343  // SortConfig returns the sort configuration.
   344  func sortConfig(provider objsort.Namespaced) objsort.Config {
   345  	return objsort.Config{
   346  		NamespacedIndicator: func(gvk schema.GroupVersionKind) (bool, error) {
   347  			ret, err := provider(gvk)
   348  			if err != nil {
   349  				return false, err
   350  			}
   351  			return ret, nil
   352  		},
   353  	}
   354  }