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  }