-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This adds "konk exec" which executes a simple konkfile.
- Loading branch information
Showing
10 changed files
with
405 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
tmp/ | ||
dist/ | ||
integration/bin/ | ||
/konkfile | ||
/konkfile.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package cmd | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/BurntSushi/toml" | ||
"github.com/jclem/konk/konk/debugger" | ||
"github.com/jclem/konk/konk/konkfile" | ||
"github.com/spf13/cobra" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
var konkfilePath string | ||
|
||
var execCommand = cobra.Command{ | ||
Use: "exec <command>", | ||
Aliases: []string{"e"}, | ||
Short: "Execute a command from a konkfile (alias: e)", | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
dbg := debugger.Get(cmd.Context()) | ||
dbg.Flags(cmd) | ||
|
||
if workingDirectory != "" { | ||
if err := os.Chdir(workingDirectory); err != nil { | ||
return fmt.Errorf("changing working directory: %w", err) | ||
} | ||
} | ||
|
||
kfsearch := []string{"konkfile", "konkfile.json", "konkfile.toml", "konkfile.yaml", "konkfile.yml"} | ||
if konkfilePath != "" { | ||
kfsearch = []string{konkfilePath} | ||
} | ||
|
||
var kf []byte | ||
var kfpath string | ||
|
||
for _, kfp := range kfsearch { | ||
b, err := os.ReadFile(kfp) | ||
if err != nil { | ||
if os.IsNotExist(err) { | ||
continue | ||
} | ||
|
||
return fmt.Errorf("reading konkfile: %w", err) | ||
} | ||
|
||
kf = b | ||
kfpath = kfp | ||
} | ||
|
||
ext := filepath.Ext(kfpath) | ||
var file konkfile.File | ||
|
||
if ext == "" { | ||
if err := json.Unmarshal(kf, &file); err != nil { | ||
if err := yaml.Unmarshal(kf, &file); err != nil { | ||
if err := toml.Unmarshal(kf, &file); err != nil { | ||
return fmt.Errorf("unmarshalling konkfile: %w", err) | ||
} | ||
} | ||
} | ||
} else if ext == ".yaml" || ext == ".yml" { | ||
if err := yaml.Unmarshal(kf, &file); err != nil { | ||
return fmt.Errorf("unmarshalling konkfile: %w", err) | ||
} | ||
} else if ext == ".toml" { | ||
if err := toml.Unmarshal(kf, &file); err != nil { | ||
return fmt.Errorf("unmarshalling konkfile: %w", err) | ||
} | ||
} else { | ||
if err := json.Unmarshal(kf, &file); err != nil { | ||
return fmt.Errorf("unmarshalling konkfile: %w", err) | ||
} | ||
} | ||
|
||
if err := konkfile.Execute(cmd.Context(), file, args[0], konkfile.ExecuteConfig{ | ||
AggregateOutput: aggregateOutput, | ||
ContinueOnError: continueOnError, | ||
NoColor: noColor, | ||
NoShell: noShell, | ||
}); err != nil { | ||
return fmt.Errorf("executing command: %w", err) | ||
} | ||
|
||
return nil | ||
}, | ||
} | ||
|
||
func init() { | ||
execCommand.Flags().StringVarP(&workingDirectory, "working-directory", "w", "", "set the working directory for all commands") | ||
execCommand.Flags().BoolVarP(&aggregateOutput, "aggregate-output", "g", false, "aggregate command output") | ||
execCommand.Flags().BoolVarP(&continueOnError, "continue-on-error", "c", false, "continue running commands after a failure") | ||
execCommand.Flags().StringVarP(&konkfilePath, "konkfile", "k", "", "path to konkfile") | ||
rootCmd.AddCommand(&execCommand) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package env | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/mattn/go-shellwords" | ||
) | ||
|
||
func Parse(env []string) ([]string, error) { | ||
parsedEnv := make([]string, 0, len(env)) | ||
|
||
// Unquote any quoted .env vars. | ||
for _, line := range env { | ||
parsed, err := shellwords.Parse(line) | ||
if err != nil { | ||
return nil, fmt.Errorf("parsing .env line: %w", err) | ||
} | ||
|
||
if len(parsed) == 0 { | ||
continue | ||
} | ||
|
||
if len(parsed) != 1 { | ||
return nil, fmt.Errorf("invalid .env line: %s", line) | ||
} | ||
|
||
parsedEnv = append(parsedEnv, parsed[0]) | ||
} | ||
|
||
return parsedEnv, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package konkfile | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/jclem/konk/konk" | ||
"github.com/jclem/konk/konk/konkfile/internal/dag" | ||
"github.com/mattn/go-shellwords" | ||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
type ExecuteConfig struct { | ||
AggregateOutput bool | ||
ContinueOnError bool | ||
NoColor bool | ||
NoShell bool | ||
} | ||
|
||
func Execute(ctx context.Context, file File, command string, cfg ExecuteConfig) error { | ||
g := dag.New[string]() | ||
|
||
for name := range file.Commands { | ||
g.AddNode(name) | ||
} | ||
|
||
for name, cmd := range file.Commands { | ||
for _, dep := range cmd.Needs { | ||
if err := g.AddEdge(name, dep); err != nil { | ||
return fmt.Errorf("adding edge: %w", err) | ||
} | ||
} | ||
} | ||
|
||
s := &scheduler{wgs: make(map[string]*sync.WaitGroup, 0)} | ||
|
||
for _, n := range g.Nodes() { | ||
s.wgs[n] = new(sync.WaitGroup) | ||
s.wgs[n].Add(1) | ||
} | ||
|
||
mut := new(sync.Mutex) | ||
wg := new(sync.WaitGroup) | ||
|
||
ctx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
|
||
onNode := func(n string) error { | ||
mut.Lock() | ||
|
||
cmd, ok := file.Commands[n] | ||
if !ok { | ||
return fmt.Errorf("command not found: %s", n) | ||
} | ||
|
||
if cmd.Exclusive { | ||
defer mut.Unlock() | ||
wg.Wait() | ||
} else { | ||
wg.Add(1) | ||
defer wg.Done() | ||
mut.Unlock() | ||
} | ||
|
||
var c *konk.Command | ||
if cfg.NoShell { | ||
parts, err := shellwords.Parse(cmd.Run) | ||
if err != nil { | ||
return fmt.Errorf("parsing command: %w", err) | ||
} | ||
|
||
c = konk.NewCommand(konk.CommandConfig{ | ||
Name: parts[0], | ||
Args: parts[1:], | ||
Label: n, | ||
NoColor: cfg.NoColor, | ||
}) | ||
} else { | ||
c = konk.NewShellCommand(konk.ShellCommandConfig{ | ||
Command: cmd.Run, | ||
Label: n, | ||
NoColor: false, | ||
}) | ||
} | ||
|
||
if err := c.Run(ctx, cancel, konk.RunCommandConfig{ | ||
AggregateOutput: cfg.AggregateOutput, | ||
KillOnCancel: !cfg.ContinueOnError, | ||
}); err != nil { | ||
return fmt.Errorf("running command: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
path, err := g.Visit(command) | ||
if err != nil { | ||
return fmt.Errorf("visiting node: %w", err) | ||
} | ||
|
||
var eg errgroup.Group | ||
for _, n := range path { | ||
n := n | ||
eg.Go(func() error { | ||
from := g.From(n) | ||
return s.run(n, from, onNode) | ||
}) | ||
} | ||
|
||
if err := eg.Wait(); err != nil { | ||
return fmt.Errorf("running commands: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type scheduler struct { | ||
wgs map[string]*sync.WaitGroup | ||
} | ||
|
||
func (s *scheduler) run(n string, deps []string, onNode func(string) error) error { | ||
defer s.wgs[n].Done() | ||
|
||
for _, dep := range deps { | ||
s.wgs[dep].Wait() | ||
} | ||
|
||
if err := onNode(n); err != nil { | ||
return fmt.Errorf("running node: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package konkfile | ||
|
||
type File struct { | ||
Commands map[string]Command `json:"commands" toml:"commands" yaml:"commands"` | ||
} | ||
|
||
type Command struct { | ||
Run string `json:"run" toml:"run" yaml:"run"` | ||
Needs []string `json:"needs" toml:"needs" yaml:"needs"` | ||
Exclusive bool `json:"exclusive" toml:"exclusive" yaml:"exclusive"` | ||
} |
Oops, something went wrong.