github.com/adevinta/lava@v0.7.2/internal/engine/jobs.go (about)

     1  // Copyright 2023 Adevinta
     2  
     3  package engine
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"maps"
     9  	"reflect"
    10  	"slices"
    11  	"time"
    12  
    13  	"github.com/adevinta/vulcan-agent/jobrunner"
    14  	"github.com/adevinta/vulcan-agent/queue"
    15  	checkcatalog "github.com/adevinta/vulcan-check-catalog/pkg/model"
    16  	"github.com/google/uuid"
    17  
    18  	"github.com/adevinta/lava/internal/assettypes"
    19  	"github.com/adevinta/lava/internal/checktypes"
    20  	"github.com/adevinta/lava/internal/config"
    21  )
    22  
    23  // generateJobs generates the jobs to be sent to the agent.
    24  func generateJobs(catalog checktypes.Catalog, targets []config.Target) ([]jobrunner.Job, error) {
    25  	var jobs []jobrunner.Job
    26  	for _, check := range generateChecks(catalog, targets) {
    27  		// Convert the options to a marshalled json string.
    28  		jsonOpts, err := json.Marshal(check.options)
    29  		if err != nil {
    30  			return nil, fmt.Errorf("encode check options: %w", err)
    31  		}
    32  
    33  		var reqVars []string
    34  		if check.checktype.RequiredVars != nil {
    35  			// TODO(sg): find out why the type of
    36  			// github.com/adevinta/vulcan-check-catalog/pkg/model.Checktype.RequiredVars
    37  			// is interface{}.
    38  			ctReqVars, ok := check.checktype.RequiredVars.([]any)
    39  			if !ok {
    40  				return nil, fmt.Errorf("invalid required vars type: %#v", ctReqVars)
    41  			}
    42  
    43  			for _, rv := range ctReqVars {
    44  				v, ok := rv.(string)
    45  				if !ok {
    46  					return nil, fmt.Errorf("invalid var type: %#v", rv)
    47  				}
    48  				reqVars = append(reqVars, v)
    49  			}
    50  		}
    51  
    52  		jobs = append(jobs, jobrunner.Job{
    53  			CheckID:      check.id,
    54  			Image:        check.checktype.Image,
    55  			Target:       check.target.Identifier,
    56  			Timeout:      check.checktype.Timeout,
    57  			AssetType:    string(check.target.AssetType),
    58  			Options:      string(jsonOpts),
    59  			RequiredVars: reqVars,
    60  		})
    61  	}
    62  	return jobs, nil
    63  }
    64  
    65  // check represents an instance of a checktype.
    66  type check struct {
    67  	id        string
    68  	checktype checkcatalog.Checktype
    69  	target    config.Target
    70  	options   map[string]interface{}
    71  }
    72  
    73  // generateChecks generates a list of checks combining a map of
    74  // checktypes and a list of targets.
    75  func generateChecks(catalog checktypes.Catalog, targets []config.Target) []check {
    76  	var checks []check
    77  	for _, t := range dedup(targets) {
    78  		for _, ct := range catalog {
    79  			at := assettypes.ToVulcan(t.AssetType)
    80  			if !checktypes.Accepts(ct, at) {
    81  				continue
    82  			}
    83  
    84  			// Merge target and check options. Target
    85  			// options take precedence for being more
    86  			// restrictive.
    87  			opts := make(map[string]interface{})
    88  			maps.Copy(opts, ct.Options)
    89  			maps.Copy(opts, t.Options)
    90  			checks = append(checks, check{
    91  				id:        uuid.New().String(),
    92  				checktype: ct,
    93  				target:    t,
    94  				options:   opts,
    95  			})
    96  		}
    97  	}
    98  	return checks
    99  }
   100  
   101  // dedup returns a deduplicated slice.
   102  func dedup[S ~[]E, E any](s S) S {
   103  	var ret S
   104  	for _, v := range s {
   105  		if !contains(ret, v) {
   106  			ret = append(ret, v)
   107  		}
   108  	}
   109  	return ret
   110  }
   111  
   112  // contains reports whether v is present in s. It uses
   113  // [reflect.DeepEqual] to compare elements.
   114  func contains[S ~[]E, E any](s S, v E) bool {
   115  	return slices.ContainsFunc(s, func(e E) bool {
   116  		return reflect.DeepEqual(e, v)
   117  	})
   118  }
   119  
   120  // sendJobs feeds the provided queue with jobs.
   121  func sendJobs(jobs []jobrunner.Job, qw queue.Writer) error {
   122  	for _, job := range jobs {
   123  		job.StartTime = time.Now()
   124  		bytes, err := json.Marshal(job)
   125  		if err != nil {
   126  			return fmt.Errorf("marshal json: %w", err)
   127  		}
   128  		if err := qw.Write(string(bytes)); err != nil {
   129  			return fmt.Errorf("queue write: %w", err)
   130  		}
   131  	}
   132  	return nil
   133  }