Skip to content

Commit 6421a22

Browse files
authored
Allow multiple exec commands (#307)
1 parent d96d263 commit 6421a22

File tree

8 files changed

+169
-17
lines changed

8 files changed

+169
-17
lines changed

cmd/litefs/config.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ import (
2121

2222
// Config represents a configuration for the binary process.
2323
type Config struct {
24-
Exec string `yaml:"exec"`
25-
ExitOnError bool `yaml:"exit-on-error"`
26-
SkipSync bool `yaml:"skip-sync"`
27-
StrictVerify bool `yaml:"strict-verify"`
24+
ExitOnError bool `yaml:"exit-on-error"`
25+
SkipSync bool `yaml:"skip-sync"`
26+
StrictVerify bool `yaml:"strict-verify"`
27+
28+
Exec ExecConfigSlice `yaml:"exec"`
2829

2930
Data DataConfig `yaml:"data"`
3031
FUSE FUSEConfig `yaml:"fuse"`
@@ -56,6 +57,27 @@ func NewConfig() Config {
5657
return config
5758
}
5859

60+
// ExecConfigSlice represents a wrapper type for handling YAML marshaling.
61+
type ExecConfigSlice []*ExecConfig
62+
63+
func (a *ExecConfigSlice) UnmarshalYAML(value *yaml.Node) error {
64+
switch value.Tag {
65+
case "!!str":
66+
*a = ExecConfigSlice{{Cmd: value.Value}}
67+
return nil
68+
case "!!seq":
69+
return value.Decode((*[]*ExecConfig)(a))
70+
default:
71+
return fmt.Errorf("invalid exec config format")
72+
}
73+
}
74+
75+
// ExecConfig represents a single exec command.
76+
type ExecConfig struct {
77+
Cmd string `yaml:"cmd"`
78+
IfCandidate bool `yaml:"if-candidate"`
79+
}
80+
5981
// DataConfig represents the configuration for internal LiteFS data. This
6082
// includes database files as well as LTX transaction files.
6183
type DataConfig struct {

cmd/litefs/config_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package main_test
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
main "github.com/superfly/litefs/cmd/litefs"
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
func TestConfig(t *testing.T) {
13+
t.Run("EmptyExec", func(t *testing.T) {
14+
var config main.Config
15+
dec := yaml.NewDecoder(strings.NewReader("exec:\n"))
16+
dec.KnownFields(true)
17+
if err := dec.Decode(&config); err != nil {
18+
t.Fatal(err)
19+
}
20+
21+
if got, want := len(config.Exec), 0; got != want {
22+
t.Fatalf("len=%v, want %v", got, want)
23+
}
24+
})
25+
26+
t.Run("InlineExec", func(t *testing.T) {
27+
var config main.Config
28+
dec := yaml.NewDecoder(strings.NewReader(`exec: "run me"`))
29+
dec.KnownFields(true)
30+
if err := dec.Decode(&config); err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
if got, want := config.Exec[0].Cmd, "run me"; got != want {
35+
t.Fatalf("Cmd=%q, want %q", got, want)
36+
}
37+
})
38+
39+
t.Run("SingleExec", func(t *testing.T) {
40+
buf, err := testdata.ReadFile("testdata/config/single_exec.yml")
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
45+
var config main.Config
46+
dec := yaml.NewDecoder(bytes.NewReader(buf))
47+
dec.KnownFields(true)
48+
if err := dec.Decode(&config); err != nil {
49+
t.Fatal(err)
50+
}
51+
52+
if got, want := config.Exec[0].Cmd, "run me"; got != want {
53+
t.Fatalf("Cmd=%q, want %q", got, want)
54+
}
55+
})
56+
57+
t.Run("MultiExec", func(t *testing.T) {
58+
buf, err := testdata.ReadFile("testdata/config/multi_exec.yml")
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
63+
var config main.Config
64+
dec := yaml.NewDecoder(bytes.NewReader(buf))
65+
dec.KnownFields(true)
66+
if err := dec.Decode(&config); err != nil {
67+
t.Fatal(err)
68+
}
69+
70+
if got, want := config.Exec[0].Cmd, "run me"; got != want {
71+
t.Fatalf("Cmd=%q, want %q", got, want)
72+
} else if got, want := config.Exec[0].IfCandidate, true; got != want {
73+
t.Fatalf("IfCandidate=%v, want %v", got, want)
74+
}
75+
if got, want := config.Exec[1].Cmd, "run me too"; got != want {
76+
t.Fatalf("Cmd=%q, want %q", got, want)
77+
}
78+
})
79+
}

cmd/litefs/export_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// go:build linux
12
package main_test
23

34
import (

cmd/litefs/import_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// go:build linux
12
package main_test
23

34
import (

cmd/litefs/main_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main_test
22

33
import (
4+
"embed"
45
"flag"
56
"log"
67
"os"
@@ -9,6 +10,9 @@ import (
910
"github.com/superfly/litefs"
1011
)
1112

13+
//go:embed testdata
14+
var testdata embed.FS
15+
1216
var (
1317
fuseDebug = flag.Bool("fuse.debug", false, "enable fuse debugging")
1418
tracing = flag.Bool("tracing", false, "enable trace logging")

cmd/litefs/mount_linux.go

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Arguments:
9393

9494
// Override "exec" field if specified on the CLI.
9595
if args1 != nil {
96-
c.Config.Exec = strings.Join(args1, " ")
96+
c.Config.Exec = ExecConfigSlice{{Cmd: strings.Join(args1, " ")}}
9797
}
9898

9999
// Override "debug" field if specified on the CLI.
@@ -260,8 +260,11 @@ func (c *MountCommand) Run(ctx context.Context) (err error) {
260260
}
261261

262262
// Execute subcommand, if specified in config.
263-
if err := c.execCmd(ctx); err != nil {
264-
return fmt.Errorf("cannot exec: %w", err)
263+
// Exit if no subcommand specified.
264+
if len(c.Config.Exec) > 0 {
265+
if err := c.execCmds(ctx); err != nil {
266+
return fmt.Errorf("cannot exec: %w", err)
267+
}
265268
}
266269

267270
return nil
@@ -384,21 +387,56 @@ func (c *MountCommand) initProxyServer(ctx context.Context) error {
384387
return nil
385388
}
386389

387-
func (c *MountCommand) execCmd(ctx context.Context) error {
388-
// Exit if no subcommand specified.
389-
if c.Config.Exec == "" {
390-
return nil
390+
// execCmds sequentially executes the commands in the "exec" config.
391+
// The last command is run asynchronously and will send its exit to the execCh.
392+
func (c *MountCommand) execCmds(ctx context.Context) error {
393+
for i, config := range c.Config.Exec {
394+
args, err := shellwords.Parse(config.Cmd)
395+
if err != nil {
396+
return fmt.Errorf("cannot parse exec command[%d]: %w", i, err)
397+
}
398+
cmd, args := args[0], args[1:]
399+
400+
// Skip if command should only run on candidate nodes and this is a non-candidate.
401+
if config.IfCandidate && !c.Store.Candidate() {
402+
log.Printf("node is not a candidate, skipping command execution: %s %v", cmd, args)
403+
continue
404+
}
405+
406+
// Execute all commands synchronously except for the last one.
407+
// This is to support migration commands that occur before the app start.
408+
if i < len(c.Config.Exec)-1 {
409+
if err := c.execSyncCmd(ctx, cmd, args); err != nil {
410+
return fmt.Errorf("sync cmd: %w", err)
411+
}
412+
} else {
413+
if err := c.execBackgroundCmd(ctx, cmd, args); err != nil {
414+
return fmt.Errorf("background cmd: %w", err)
415+
}
416+
}
391417
}
392418

393-
// Execute subcommand process.
394-
args, err := shellwords.Parse(c.Config.Exec)
395-
if err != nil {
396-
return fmt.Errorf("cannot parse exec command: %w", err)
419+
return nil
420+
}
421+
422+
func (c *MountCommand) execSyncCmd(ctx context.Context, cmd string, args []string) error {
423+
log.Printf("executing command: %s %v", cmd, args)
424+
425+
c.cmd = exec.CommandContext(ctx, cmd, args...)
426+
c.cmd.Env = os.Environ()
427+
c.cmd.Stdout = os.Stdout
428+
c.cmd.Stderr = os.Stderr
429+
if err := c.cmd.Run(); err != nil {
430+
return fmt.Errorf("cannot run command: %w", err)
397431
}
398432

399-
log.Printf("starting subprocess: %s %v", args[0], args[1:])
433+
return nil
434+
}
435+
436+
func (c *MountCommand) execBackgroundCmd(ctx context.Context, cmd string, args []string) error {
437+
log.Printf("starting background subprocess: %s %v", cmd, args)
400438

401-
c.cmd = exec.CommandContext(ctx, args[0], args[1:]...)
439+
c.cmd = exec.CommandContext(ctx, cmd, args...)
402440
c.cmd.Env = os.Environ()
403441
c.cmd.Stdout = os.Stdout
404442
c.cmd.Stderr = os.Stderr
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
exec:
2+
- cmd: "run me"
3+
if-candidate: true
4+
5+
- cmd: "run me too"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
exec:
2+
- cmd: "run me"

0 commit comments

Comments
 (0)