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 }