github.com/splunk/dan1-qbec@v0.7.3/internal/eval/eval.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 eval encapsulates the manner in which components and parameters are evaluated for qbec. 18 package eval 19 20 import ( 21 "encoding/json" 22 "fmt" 23 "io/ioutil" 24 "sort" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/pkg/errors" 30 "github.com/splunk/qbec/internal/model" 31 "github.com/splunk/qbec/internal/sio" 32 "github.com/splunk/qbec/internal/vm" 33 ) 34 35 const ( 36 defaultConcurrency = 5 37 maxDisplayErrors = 3 38 postprocessTLAVAr = "object" 39 ) 40 41 // VMConfigFunc is a function that returns a VM configuration containing only the 42 // specified top-level variables of interest. 43 type VMConfigFunc func(tlaVars []string) vm.Config 44 45 type postProc struct { 46 ctx Context 47 code string 48 file string 49 } 50 51 func (p postProc) run(obj map[string]interface{}) (map[string]interface{}, error) { 52 if p.code == "" { 53 return obj, nil 54 } 55 b, err := json.Marshal(obj) 56 if err != nil { 57 return nil, errors.Wrap(err, "json marshal") 58 } 59 cfg := p.ctx.baseVMConfig(nil).WithTopLevelCodeVars(map[string]string{ 60 postprocessTLAVAr: string(b), 61 }) 62 63 jvm := vm.New(cfg) 64 evalCode, err := jvm.EvaluateSnippet(p.file, p.code) 65 if err != nil { 66 return nil, errors.Wrap(err, "post-eval object") 67 } 68 69 var data interface{} 70 if err := json.Unmarshal([]byte(evalCode), &data); err != nil { 71 return nil, errors.Wrap(err, fmt.Sprintf("unexpected unmarshal '%s'", p.file)) 72 } 73 t, ok := data.(map[string]interface{}) 74 if !ok { 75 return nil, fmt.Errorf("post-eval did not return an object, %s", evalCode) 76 } 77 if getRawObjectType(t) != leafType { 78 return nil, fmt.Errorf("post-eval did not return a K8s object, %s", evalCode) 79 } 80 return t, nil 81 } 82 83 // Context is the evaluation context 84 type Context struct { 85 App string // the application for which the evaluation is done 86 Tag string // the gc tag if present 87 Env string // the environment for which the evaluation is done 88 DefaultNs string // the default namespace to expose as an external variable 89 VMConfig VMConfigFunc // the base VM config to use for eval 90 Verbose bool // show generated code 91 Concurrency int // concurrent components to evaluate, default 5 92 PostProcessFile string // the file that contains post-processing code for all objects 93 CleanMode bool // whether clean mode is enabled 94 } 95 96 func (c Context) baseVMConfig(tlas []string) vm.Config { 97 fn := c.VMConfig 98 if fn == nil { 99 fn = defaultFunc 100 } 101 cm := "off" 102 if c.CleanMode { 103 cm = "on" 104 } 105 cfg := fn(tlas).WithVars(map[string]string{ 106 model.QbecNames.EnvVarName: c.Env, 107 model.QbecNames.TagVarName: c.Tag, 108 model.QbecNames.DefaultNsVarName: c.DefaultNs, 109 model.QbecNames.CleanModeVarName: cm, 110 }) 111 return cfg 112 } 113 114 func (c Context) vm(tlas []string) *vm.VM { 115 return vm.New(c.baseVMConfig(tlas)) 116 } 117 118 func (c Context) postProcessor() (postProc, error) { 119 if c.PostProcessFile == "" { 120 return postProc{}, nil 121 } 122 b, err := ioutil.ReadFile(c.PostProcessFile) 123 if err != nil { 124 return postProc{}, errors.Wrap(err, "read post-eval file") 125 } 126 return postProc{ 127 ctx: c, 128 code: string(b), 129 file: c.PostProcessFile, 130 }, nil 131 } 132 133 var defaultFunc = func(_ []string) vm.Config { return vm.Config{} } 134 135 // Components evaluates the specified components using the specific runtime 136 // parameters file and returns the result. 137 func Components(components []model.Component, ctx Context) (_ []model.K8sLocalObject, finalErr error) { 138 start := time.Now() 139 defer func() { 140 if finalErr == nil { 141 sio.Debugf("%d components evaluated in %v\n", len(components), time.Since(start).Round(time.Millisecond)) 142 } 143 }() 144 pe, err := ctx.postProcessor() 145 if err != nil { 146 return nil, err 147 } 148 ret, err := evalComponents(components, ctx, pe) 149 if err != nil { 150 return nil, err 151 } 152 153 sort.Slice(ret, func(i, j int) bool { 154 left := ret[i] 155 right := ret[j] 156 leftKey := fmt.Sprintf("%s:%s:%s:%s", left.Component(), left.GetNamespace(), left.GroupVersionKind().Kind, left.GetName()) 157 rightKey := fmt.Sprintf("%s:%s:%s:%s", right.Component(), right.GetNamespace(), right.GroupVersionKind().Kind, right.GetName()) 158 return leftKey < rightKey 159 }) 160 return ret, nil 161 } 162 163 // Params evaluates the supplied parameters file in the supplied VM and 164 // returns it as a JSON object. 165 func Params(file string, ctx Context) (map[string]interface{}, error) { 166 jvm := ctx.vm(nil) 167 code := fmt.Sprintf("import '%s'", file) 168 if ctx.Verbose { 169 sio.Debugln("Eval params:\n" + code) 170 } 171 output, err := jvm.EvaluateSnippet("param-loader.jsonnet", code) 172 if err != nil { 173 return nil, err 174 } 175 if ctx.Verbose { 176 sio.Debugln("Eval params output:\n" + prettyJSON(output)) 177 } 178 var ret map[string]interface{} 179 if err := json.Unmarshal([]byte(output), &ret); err != nil { 180 return nil, err 181 } 182 return ret, nil 183 } 184 185 func evalComponent(ctx Context, c model.Component, pe postProc) ([]model.K8sLocalObject, error) { 186 jvm := ctx.vm(c.TopLevelVars) 187 var inputCode string 188 contextFile := c.File 189 switch { 190 case strings.HasSuffix(c.File, ".yaml"): 191 inputCode = fmt.Sprintf("std.native('parseYaml')(importstr '%s')", c.File) 192 contextFile = "yaml-loader.jsonnet" 193 case strings.HasSuffix(c.File, ".json"): 194 inputCode = fmt.Sprintf("std.native('parseJson')(importstr '%s')", c.File) 195 contextFile = "json-loader.jsonnet" 196 default: 197 b, err := ioutil.ReadFile(c.File) 198 if err != nil { 199 return nil, errors.Wrap(err, "read inputCode for "+c.File) 200 } 201 inputCode = string(b) 202 } 203 evalCode, err := jvm.EvaluateSnippet(contextFile, inputCode) 204 if err != nil { 205 return nil, errors.Wrap(err, fmt.Sprintf("evaluate '%s'", c.Name)) 206 } 207 var data interface{} 208 if err := json.Unmarshal([]byte(evalCode), &data); err != nil { 209 return nil, errors.Wrap(err, fmt.Sprintf("unexpected unmarshal '%s'", c.File)) 210 } 211 212 objs, err := walk(data) 213 if err != nil { 214 return nil, errors.Wrap(err, "extract objects") 215 } 216 217 var processed []model.K8sLocalObject 218 for _, o := range objs { 219 proc, err := pe.run(o) 220 if err != nil { 221 return nil, err 222 } 223 processed = append(processed, model.NewK8sLocalObject(proc, ctx.App, ctx.Tag, c.Name, ctx.Env)) 224 } 225 return processed, nil 226 } 227 228 func evalComponents(list []model.Component, ctx Context, pe postProc) ([]model.K8sLocalObject, error) { 229 var ret []model.K8sLocalObject 230 if len(list) == 0 { 231 return ret, nil 232 } 233 234 ch := make(chan model.Component, len(list)) 235 for _, c := range list { 236 ch <- c 237 } 238 close(ch) 239 240 var errs []error 241 var l sync.Mutex 242 243 concurrency := ctx.Concurrency 244 if concurrency <= 0 { 245 concurrency = defaultConcurrency 246 } 247 if concurrency > len(list) { 248 concurrency = len(list) 249 } 250 var wg sync.WaitGroup 251 wg.Add(concurrency) 252 253 for i := 0; i < concurrency; i++ { 254 go func() { 255 defer wg.Done() 256 for c := range ch { 257 objs, err := evalComponent(ctx, c, pe) 258 l.Lock() 259 if err != nil { 260 errs = append(errs, err) 261 } else { 262 ret = append(ret, objs...) 263 } 264 l.Unlock() 265 } 266 }() 267 } 268 wg.Wait() 269 if len(errs) > 0 { 270 var msgs []string 271 for i, e := range errs { 272 if i == maxDisplayErrors { 273 msgs = append(msgs, fmt.Sprintf("... and %d more errors", len(errs)-maxDisplayErrors)) 274 break 275 } 276 msgs = append(msgs, e.Error()) 277 } 278 return nil, errors.New(strings.Join(msgs, "\n")) 279 } 280 return ret, nil 281 } 282 283 func prettyJSON(s string) string { 284 var data interface{} 285 if err := json.Unmarshal([]byte(s), &data); err == nil { 286 b, err := json.MarshalIndent(data, "", " ") 287 if err == nil { 288 return string(b) 289 } 290 } 291 return s 292 }