github.com/davinci-std/kanvas@v0.11.1/interpreter/interpreter.go (about) 1 package interpreter 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/davinci-std/kanvas" 10 11 "github.com/hashicorp/go-multierror" 12 "github.com/mumoshu/kargo" 13 ) 14 15 type WorkflowJob struct { 16 ID string 17 Outputs map[string]string 18 Ran bool 19 20 *kanvas.WorkflowJob 21 } 22 23 type Interpreter struct { 24 Workflow *kanvas.Workflow 25 WorkflowJobs map[string]*WorkflowJob 26 runtime *kanvas.Runtime 27 28 EnableParallel bool 29 } 30 31 func New(wf *kanvas.Workflow, r *kanvas.Runtime) *Interpreter { 32 wjs := map[string]*WorkflowJob{} 33 for k, v := range wf.WorkflowJobs { 34 v := v 35 wjs[k] = &WorkflowJob{ 36 ID: k, 37 Outputs: make(map[string]string), 38 WorkflowJob: v, 39 } 40 } 41 42 return &Interpreter{ 43 Workflow: wf, 44 WorkflowJobs: wjs, 45 runtime: r, 46 } 47 } 48 49 func (p *Interpreter) Run(f func(job *WorkflowJob) error) error { 50 for _, phase := range p.Workflow.Plan { 51 if err := p.parallel(phase, f); err != nil { 52 return err 53 } 54 } 55 56 return nil 57 } 58 59 func (p *Interpreter) run(name string, f func(job *WorkflowJob) error) error { 60 job, ok := p.WorkflowJobs[name] 61 if !ok { 62 return fmt.Errorf("component %q is not defined", name) 63 } 64 65 if job.Skipped != nil { 66 job.Outputs = job.Skipped 67 return nil 68 } 69 70 if err := f(job); err != nil { 71 return fmt.Errorf("component %q: %w", name, err) 72 } 73 74 return nil 75 } 76 77 func (p *Interpreter) parallel(names []string, f func(job *WorkflowJob) error) error { 78 var ( 79 errs error 80 errCh = make(chan error, len(names)) 81 ) 82 83 for _, n := range names { 84 n := n 85 if p.EnableParallel { 86 go func() { 87 errCh <- p.run(n, f) 88 }() 89 } else { 90 errCh <- p.run(n, f) 91 } 92 } 93 94 for i := 0; i < len(names); i++ { 95 if err := <-errCh; err != nil { 96 errs = multierror.Append(errs, err) 97 } 98 } 99 100 if errs != nil { 101 return fmt.Errorf("failed running component group %v: %s", names, errs) 102 } 103 104 return nil 105 } 106 107 func (p *Interpreter) runWithExtraArgs(j *WorkflowJob, op kanvas.Op, steps []kanvas.Step) error { 108 outputs := map[string]string{} 109 for _, step := range steps { 110 if step.IfOutputEq.Key != "" { 111 if step.IfOutputEq.Value != outputs[step.IfOutputEq.Key] { 112 continue 113 } 114 } 115 116 for _, c := range step.Run { 117 if err := p.runCmd(j, c); err != nil { 118 return fmt.Errorf("command %s: %w", c, err) 119 } 120 } 121 122 if step.OutputFunc != nil { 123 if err := step.OutputFunc(p.runtime, outputs); err != nil { 124 return err 125 } 126 } 127 } 128 129 if j.Driver.OutputFunc != nil { 130 if err := j.Driver.OutputFunc(p.runtime, op, outputs); err != nil { 131 return err 132 } 133 } 134 135 j.Outputs = outputs 136 137 return nil 138 } 139 140 func (p *Interpreter) runCmd(j *WorkflowJob, cmd kargo.Cmd) error { 141 args, err := cmd.Args.Collect(func(out string) (string, error) { 142 jobOutput := strings.SplitN(out, ".", 2) 143 if len(jobOutput) != 2 { 144 return "", fmt.Errorf("could not find dot(.) within %q", out) 145 } 146 jobName := jobOutput[0] 147 outName := jobOutput[1] 148 149 fullJobName := kanvas.SiblingID(j.ID, jobName) 150 151 job, ok := p.WorkflowJobs[fullJobName] 152 if !ok { 153 return "", fmt.Errorf("job %q does not exist", jobName) 154 } 155 156 val, ok := job.Outputs[outName] 157 if !ok { 158 var debug string 159 if os.Getenv("DEBUG") == "1" { 160 debug = fmt.Sprintf(". Available outputs: %v", job.Outputs) 161 } else { 162 debug = ". Set DEBUG=1 to see all the outputs" 163 } 164 return "", fmt.Errorf(`output "%s.%s" does not exist. Ensure that %q outputs %q%s`, jobName, outName, jobName, outName, debug) 165 } 166 167 return val, nil 168 }) 169 if err != nil { 170 return fmt.Errorf("while collecting args for command %q: %w", cmd.Name, err) 171 } 172 173 c := []string{cmd.Name} 174 c = append(c, args...) 175 176 dir := cmd.Dir 177 if dir == "" { 178 dir = j.Dir 179 } 180 181 var opts []kanvas.ExecOption 182 if len(cmd.AddEnv) > 0 { 183 opts = append(opts, kanvas.ExecAddEnv(cmd.AddEnv)) 184 } 185 186 if err := p.runtime.Exec(dir, c, opts...); err != nil { 187 return fmt.Errorf("command %q: %w", cmd.Name, err) 188 } 189 190 return nil 191 } 192 193 func (p *Interpreter) Apply() error { 194 if err := p.Run(func(job *WorkflowJob) error { 195 if err := p.applyJob(job); err != nil { 196 return err 197 } 198 199 return nil 200 }); err != nil { 201 return err 202 } 203 204 // An example of the whole kanvas standard output would look like: 205 // { 206 // "app": { 207 // "pullRequest.head": "kanvas-20231216082910", 208 // "pullRequest.htmlURL": "https://github.com/myorg/mygitopsconfig/pull/123", 209 // "pullRequest.id": "1234567890", 210 // "pullRequest.number": "123" 211 // }, 212 // "git": { 213 // "sha": "123d3c1a9a669f45c17a25cf5222c0cc0b630738", 214 // "tag": "" 215 // }, 216 // "image": { 217 // "id": "sha256:12356935acbd6a67d3fed10512d89450330047f3ae6fb3a62e9bf4f229529387", 218 // "kanvas.buildx": "true" 219 // } 220 // } 221 222 out := map[string]map[string]string{} 223 224 for _, job := range p.WorkflowJobs { 225 out[job.ID] = job.Outputs 226 } 227 228 res, err := json.MarshalIndent(out, "", " ") 229 if err != nil { 230 return fmt.Errorf("marshaling outputs: %w", err) 231 } 232 233 fmt.Fprintf(os.Stdout, "%s\n", res) 234 235 return nil 236 } 237 238 func (p *Interpreter) Diff() error { 239 return p.Run(func(job *WorkflowJob) error { 240 return p.diffJob(job) 241 }) 242 } 243 244 func (p *Interpreter) diffJob(j *WorkflowJob) error { 245 if j.Ran { 246 return nil 247 } 248 249 if err := p.runWithExtraArgs(j, kanvas.Diff, j.Driver.Diff); err != nil { 250 return err 251 } 252 253 j.Ran = true 254 255 return nil 256 } 257 258 func (p *Interpreter) applyJob(j *WorkflowJob) error { 259 if j.Ran { 260 return nil 261 } 262 263 if err := p.runWithExtraArgs(j, kanvas.Apply, j.Driver.Apply); err != nil { 264 return err 265 } 266 267 j.Ran = true 268 269 return nil 270 }