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 }