Skip to content

Commit 6f0362a

Browse files
authored
feat: add process-compose command to CLI (#6)
- Add `process-compose` command to start a `process-compose` easily which was configured in a devenv Nix shell by doing `quitsh process-compose start ./tools/nix#mynamespace.shells.test-db`.
1 parent 99cb3db commit 6f0362a

11 files changed

Lines changed: 281 additions & 23 deletions

File tree

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ format *args:
3838
package-nix:
3939
nix build -L "{{flake_dir}}#cli" -o "{{out_dir}}/package/cli"
4040

41+
# Clean the output folder.
42+
clean:
43+
rm -rf "{{out_dir}}"
44+
4145
## CI =========================================================================
4246
ci *args:
4347
just nix-develop "ci" "$@"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package processcompose
2+
3+
import (
4+
"errors"
5+
6+
"github.com/sdsc-ordes/quitsh/pkg/cli"
7+
processcomposestart "github.com/sdsc-ordes/quitsh/pkg/cli/cmd/process-compose/start"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func AddCmd(cl cli.ICLI, parent *cobra.Command, flakeDirDefault string) *cobra.Command {
13+
configCmd := &cobra.Command{
14+
Use: "process-compose",
15+
Short: "Start process-compose services defined in Nix 'devenv.sh' shells.",
16+
RunE: func(_cmd *cobra.Command, _args []string) error {
17+
return errors.New("no subcommand given")
18+
},
19+
}
20+
21+
processcomposestart.AddCmd(cl, configCmd, flakeDirDefault)
22+
23+
parent.AddCommand(configCmd)
24+
25+
return configCmd
26+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package processcomposestart
2+
3+
import (
4+
"context"
5+
"os"
6+
"path"
7+
"strings"
8+
"time"
9+
10+
"github.com/sdsc-ordes/quitsh/pkg/cli"
11+
"github.com/sdsc-ordes/quitsh/pkg/errors"
12+
processcompose "github.com/sdsc-ordes/quitsh/pkg/exec/process-compose"
13+
fs "github.com/sdsc-ordes/quitsh/pkg/filesystem"
14+
"github.com/sdsc-ordes/quitsh/pkg/log"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
const longDesc = `Start a process-compose definition from a 'devenv.sh' Nix shell
19+
specified by an attribute path (e.g. 'mynamespace.shells.test-dbs') or installable
20+
(e.g. './tools/nix#mynamespace.shells.test-dbs')
21+
in a 'flake.nix' file.`
22+
23+
const timeoutContainers = 100 * time.Second
24+
25+
type startArgs struct {
26+
attrPath string
27+
flakeDir string
28+
waitFor []string
29+
30+
socketPathFile string
31+
}
32+
33+
func AddCmd(cl cli.ICLI, parent *cobra.Command, defaultFlakeDir string) {
34+
var stArgs startArgs
35+
36+
startCmd := &cobra.Command{
37+
Use: "start [devenv-attr-path or devenv-installable]",
38+
Short: "Start a process-compose definition from a 'devenv.sh' Nix shell.",
39+
Long: longDesc,
40+
PreRunE: cobra.MinimumNArgs(1),
41+
RunE: func(_cmd *cobra.Command, args []string) error {
42+
stArgs.attrPath = args[0]
43+
44+
_, err := StartServices(
45+
cl.RootDir(),
46+
stArgs.flakeDir,
47+
stArgs.attrPath,
48+
stArgs.waitFor,
49+
stArgs.socketPathFile)
50+
51+
return err
52+
},
53+
}
54+
55+
startCmd.Flags().
56+
StringVarP(&stArgs.flakeDir,
57+
"flake-dir", "f", defaultFlakeDir, "The flake directory which contains a `flake.nix` file.")
58+
59+
startCmd.Flags().
60+
StringArrayVarP(&stArgs.waitFor,
61+
"wait-for", "w", nil, "Wait for this processes to be running.")
62+
63+
startCmd.Flags().
64+
StringVarP(&stArgs.socketPathFile,
65+
"socketPathFile", "s", ".pc-socket-path", "The file (JSON) where the process-compose socket path is written to.")
66+
67+
parent.AddCommand(startCmd)
68+
}
69+
70+
// StartServices starts the process-compose services from `flake.nix` in `flakeDir`
71+
// defined in the installable `devenvShellInstallable`.
72+
// You can wait for the processes names to be running with `waitFor`.
73+
func StartServices(
74+
rootDir string,
75+
flakeDir string,
76+
devenvShellAttrPath string,
77+
waitFor []string,
78+
socketPathFile string) (
79+
pcCtx processcompose.ProcessComposeCtx,
80+
err error,
81+
) {
82+
dir, err := os.MkdirTemp(os.TempDir(), "process-compose-*")
83+
if err != nil {
84+
return pcCtx, errors.AddContext(err, "could not create process-compose log file.")
85+
}
86+
87+
logFile := path.Join(dir, "process-compose.log")
88+
if strings.Contains(devenvShellAttrPath, "#") {
89+
pcCtx, err = processcompose.StartFromInstallable(rootDir, devenvShellAttrPath, logFile)
90+
} else {
91+
pcCtx, err = processcompose.Start(rootDir, flakeDir, devenvShellAttrPath, logFile)
92+
}
93+
94+
if err != nil {
95+
return pcCtx, errors.AddContext(err, "could not start process-compose")
96+
}
97+
98+
ctx, cancel := context.WithTimeout(context.Background(), timeoutContainers)
99+
defer cancel()
100+
101+
isRunning, err := pcCtx.WaitTillRunning(ctx, waitFor...)
102+
if err != nil || !isRunning {
103+
return pcCtx, errors.AddContext(err, "failed to wait for processes '%q'.", waitFor)
104+
}
105+
106+
err = os.WriteFile(socketPathFile, []byte(pcCtx.Socket()), fs.DefaultPermissionsFile)
107+
if err != nil {
108+
log.WarnE(err, "Could not write socket path to file '%s'.", socketPathFile)
109+
}
110+
111+
log.Info("Inspect processes with 'process-compose attach -u '%s'.", pcCtx.Socket())
112+
log.Info("Stop processes with 'process-compose down -u '%s'.", pcCtx.Socket())
113+
114+
return pcCtx, nil
115+
}

pkg/cli/cmd/root/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func New(
126126
StringVar(&rootArgs.ConfigUser, "config-user", "", "The global user configuration file (overlay), can not exist.")
127127
rootCmd.PersistentFlags().
128128
StringVarP(&rootArgs.Cwd,
129-
"cwd", "C", "",
129+
"cwd", "C", ".",
130130
"Set the current working directory "+
131131
"(note: '--root-dir' = Git root dir evaluated from `--cwd`).")
132132
rootCmd.PersistentFlags().

pkg/exec/nix/attr-paths.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import (
77

88
// FlakeInstallable returns the attribute path `<flakePath>#<attrPath>`.
99
func FlakeInstallable(flakePath string, attrPath string) string {
10+
if flakePath == "" {
11+
flakePath = "."
12+
}
13+
1014
return fmt.Sprintf("%s#%s", flakePath, attrPath)
1115
}
1216

pkg/exec/nix/context.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ func addDefaultArgs(rootDir string, b exec.CmdContextBuilder) exec.CmdContextBui
9292
// mode while still being able to run processes & services and modify
9393
// (some parts) of the active shell.mkdir -p .devenv/state
9494
// See: https://github.com/cachix/devenv/issues/1461
95+
// NOTE: This will also work if no input matches the override which is important
96+
// for users not using this.
9597
return b.
9698
Cwd(rootDir).
9799
BaseArgs(

pkg/exec/process-compose/compose.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,34 @@ type ProcessComposeCtx struct {
2323
}
2424

2525
// Start starts the process compose from a
26-
// `devShellInstallable` (e.g. `./tools/nix#custodian.shells.test-dbs`
27-
// which must be a `devenv` shell) in the flake located
26+
// `devShellAttrPath` (e.g. `custodian.shells.test-dbs`
27+
// which must be a `devenv` shell) in the flake `flake.nix` located
2828
// at `flakeDir`. The `rootDir` is the working directory and
2929
// where the `.devenv/state/pwd` file is for `nonPureEval == false`.
30+
// Note: You also call [StartFromInstallable] and directly pass an
31+
// installable, e.g. a flake output attribute path like
32+
// `./a/b/c#mynamespace.shells.test-dbs`.
3033
func Start(
3134
rootDir string,
3235
flakeDir string,
33-
devShellInstallable string,
36+
devShellAttrPath string,
3437
logFile string,
3538
) (pc ProcessComposeCtx, err error) {
36-
devShellInstallable = nix.FlakeInstallable(flakeDir, devShellInstallable)
39+
devShellAttrPath = nix.FlakeInstallable(flakeDir, devShellAttrPath)
40+
41+
return StartFromInstallable(rootDir, devShellAttrPath, logFile)
42+
}
3743

44+
// Start starts the process compose from a Nix
45+
// `devShellInstallable` (e.g. `./tools/nix#custodian.shells.test-dbs`
46+
// which must be a `devenv` shell).
47+
// The `rootDir` is the working directory and
48+
// where the `.devenv/state/pwd` file is for `nonPureEval == false`.
49+
func StartFromInstallable(
50+
rootDir string,
51+
devShellInstallable string,
52+
logFile string,
53+
) (pc ProcessComposeCtx, err error) {
3854
procCompExe, socketPath, err := getSocketPath(devShellInstallable, rootDir)
3955
if err != nil {
4056
return pc, err
@@ -66,6 +82,8 @@ func Start(
6682
devShellInstallable,
6783
"socket",
6884
socketPath,
85+
"logFile",
86+
logFile,
6987
)
7088

7189
b = exec.NewCmdCtxBuilder().
@@ -94,6 +112,10 @@ func (pc *ProcessComposeCtx) Stop() error {
94112
func (pc *ProcessComposeCtx) WaitTillRunning(
95113
ctx context.Context,
96114
procs ...string) (isRunning bool, err error) {
115+
if len(procs) == 0 {
116+
return true, nil
117+
}
118+
97119
err = pc.waitForSocket()
98120
if err != nil {
99121
return false, err
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.devenv
2+
3+
# Generated in test.
4+
.pc-socket-path

pkg/log/log.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ func createRecord(skipFns int, level slog.Level, msg string, args ...any) slog.R
107107
return r
108108
}
109109

110-
// Trace will log an trace if trace is enabled.
110+
// Trace will log a trace info with formatting.
111+
func Tracef(msg string, args ...any) {
112+
Trace(fmt.Sprintf(msg, args...))
113+
}
114+
115+
// Trace will log a trace info.
111116
func Trace(msg string, args ...any) {
112117
msg = fmt.Sprintf(msg, args...)
113118
ctx := context.Background()
@@ -120,7 +125,12 @@ func Trace(msg string, args ...any) {
120125
_ = slog.Default().Handler().Handle(ctx, r)
121126
}
122127

123-
// Debug will log an info if debug is enabled.
128+
// Debug will log a debug info with formatting.
129+
func Debugf(msg string, args ...any) {
130+
Debug(fmt.Sprintf(msg, args...))
131+
}
132+
133+
// Debug will log a debug info.
124134
func Debug(msg string, args ...any) {
125135
ctx := context.Background()
126136
const level = slog.LevelDebug
@@ -132,6 +142,11 @@ func Debug(msg string, args ...any) {
132142
_ = slog.Default().Handler().Handle(ctx, r)
133143
}
134144

145+
// Info will log an info with formatting.
146+
func Infof(msg string, args ...any) {
147+
Info(fmt.Sprintf(msg, args...))
148+
}
149+
135150
// Info will log an info.
136151
func Info(msg string, args ...any) {
137152
ctx := context.Background()
@@ -144,35 +159,65 @@ func Info(msg string, args ...any) {
144159
_ = slog.Default().Handler().Handle(ctx, r)
145160
}
146161

147-
// Warn will log an info.
162+
// Warn will log a warning info with formatting.
163+
func Warnf(msg string, args ...any) {
164+
Warn(fmt.Sprintf(msg, args...))
165+
}
166+
167+
// Warn will log an warning info.
148168
func Warn(msg string, args ...any) {
149169
warnS(0, msg, args...)
150170
}
151171

172+
// Warn will log a warning for an error `err` with formatting.
173+
func WarnEf(err error, msg string, args ...any) {
174+
WarnE(err, fmt.Sprintf(msg, args...))
175+
}
176+
152177
// Warn will log a warning for an error `err`.
153178
func WarnE(err error, msg string, args ...any) {
154-
a := make([]interface{}, 0, 2+len(args)) //nolint: mnd
179+
a := make([]any, 0, 2+len(args)) //nolint: mnd
155180
a = append(a, "error", err)
156181
a = append(a, args...)
157182
warnS(0, msg, a...)
158183
}
159184

185+
// Error will log an error with formatting.
186+
func Errorf(msg string, args ...any) {
187+
Error(fmt.Sprintf(msg, args...))
188+
}
189+
160190
// Error will log an error.
161191
func Error(msg string, args ...any) {
162192
errorS(0, msg, args...)
163193
}
164194

195+
// Error will log an error for `err` with formatting.
196+
func ErrorEf(err error, msg string, args ...any) {
197+
ErrorE(err, fmt.Sprintf(msg, args...))
198+
}
199+
165200
// Error will log an error for `err`.
166201
func ErrorE(err error, msg string, args ...any) {
167202
errorES(0, err, msg, args...)
168203
}
169204

205+
// Panic will log and panic with formatting.
206+
func Panicf(msg string, args ...any) {
207+
Panic(fmt.Sprintf(msg, args...))
208+
}
209+
170210
// Panic will log and panic.
171211
func Panic(msg string, args ...any) {
172212
errorS(0, msg, args...)
173213
panic(msg)
174214
}
175215

216+
// Panic will log and panic with formatting.
217+
func PanicEf(err error, msg string, args ...any) {
218+
PanicE(err, fmt.Sprintf(msg, args...))
219+
}
220+
176221
// PanicE will log and panic if `err` is not `nil`.
177222
func PanicE(err error, msg string, args ...any) {
178223
if err != nil {
@@ -204,7 +249,7 @@ func errorS(skipFns int, msg string, args ...any) {
204249
}
205250

206251
func errorES(skipFns int, err error, msg string, args ...any) {
207-
a := make([]interface{}, 0, 2+len(args)) //nolint: mnd
252+
a := make([]any, 0, 2+len(args)) //nolint: mnd
208253
a = append(a, args...)
209254
a = append(a, "error", err)
210255
errorS(skipFns+1, msg, a...)

0 commit comments

Comments
 (0)