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  }