+38
-30
cmd/crons.go
+38
-30
cmd/crons.go
···
4
4
"context"
5
5
"encoding/json"
6
6
"fmt"
7
-
"io"
8
-
"log"
7
+
"log/slog"
9
8
"os"
10
9
"path/filepath"
11
10
"strings"
···
29
28
func NewCmdCrons() *cobra.Command {
30
29
var flags struct {
31
30
json bool
31
+
app string
32
32
all bool
33
33
}
34
34
35
35
cmd := &cobra.Command{
36
-
Use: "crons [app]",
37
-
Aliases: []string{"cron"},
38
-
Args: cobra.MaximumNArgs(1),
39
-
ValidArgsFunction: completeApp,
40
-
PreRunE: func(cmd *cobra.Command, args []string) error {
41
-
if len(args) > 0 && flags.all {
42
-
return fmt.Errorf("cannot set both --all and specify an app")
43
-
}
44
-
45
-
return nil
46
-
},
47
-
Short: "List cron jobs",
36
+
Use: "crons",
37
+
Aliases: []string{"cron"},
38
+
Args: cobra.NoArgs,
39
+
Short: "List cron jobs",
48
40
RunE: func(cmd *cobra.Command, args []string) error {
49
41
var crons []CronItem
50
42
apps, err := app.ListApps(k.String("dir"))
···
142
134
143
135
cmd.Flags().BoolVar(&flags.json, "json", false, "output as json")
144
136
cmd.Flags().BoolVar(&flags.all, "all", false, "show all cron jobs")
137
+
cmd.Flags().StringVarP(&flags.app, "app", "a", "", "the app to show cron jobs for")
138
+
cmd.RegisterFlagCompletionFunc("app", completeApp)
139
+
cmd.MarkFlagsMutuallyExclusive("app", "all")
145
140
146
141
return cmd
147
142
}
148
143
149
-
func CronRunner(stdout, stderr io.Writer) *cron.Cron {
144
+
func CronRunner(logger *slog.Logger) *cron.Cron {
150
145
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
151
146
c := cron.New(cron.WithParser(parser))
152
147
_, _ = c.AddFunc("* * * * *", func() {
153
-
rounded := time.Now().Truncate(time.Minute)
154
-
for _, appname := range k.MapKeys("apps") {
155
-
for _, job := range k.Slices(fmt.Sprintf("apps.%s.crons", appname)) {
156
-
sched, err := parser.Parse(job.String("schedule"))
148
+
apps, err := app.ListApps(k.String("dir"))
149
+
if err != nil {
150
+
logger.Error("failed to list apps", "error", err)
151
+
return
152
+
}
153
+
154
+
for _, appname := range apps {
155
+
a, err := app.LoadApp(appname, k.String("dir"), k.String("domain"))
156
+
if err != nil {
157
+
logger.Error("failed to load app", "app", appname, "error", err)
158
+
continue
159
+
}
160
+
161
+
current := time.Now().Truncate(time.Minute)
162
+
for _, job := range a.Config.Crons {
163
+
sched, err := parser.Parse(job.Schedule)
157
164
if err != nil {
158
-
fmt.Println(err)
165
+
logger.Error("failed to parse cron schedule", "app", appname, "schedule", job.Schedule, "error", err)
159
166
continue
160
167
}
161
168
162
-
if sched.Next(rounded.Add(-1*time.Second)) != rounded {
169
+
if sched.Next(current.Add(-1*time.Second)) != current {
163
170
continue
164
171
}
165
172
166
-
a, err := app.LoadApp(appname, k.String("rootDir"), k.String("domain"))
173
+
a, err := app.LoadApp(appname, k.String("dir"), k.String("domain"))
167
174
if err != nil {
168
-
fmt.Println(err)
175
+
logger.Error("failed to load app", "app", appname, "error", err)
169
176
continue
170
177
}
171
178
wk := worker.NewWorker(a, k.Bool(fmt.Sprintf("apps.%s.admin", a.Name)), nil)
172
179
173
-
command, err := wk.Command(context.Background(), job.Strings("args"), nil)
180
+
command, err := wk.Command(context.Background(), job.Args, nil)
174
181
if err != nil {
175
-
fmt.Println(err)
182
+
logger.Error("failed to create command", "app", appname, "args", job.Args, "error", err)
176
183
continue
177
184
}
178
-
command.Stdout = stdout
179
-
command.Stderr = stderr
180
185
181
-
if err := command.Run(); err != nil {
182
-
log.Printf("failed to run command: %v", err)
183
-
}
186
+
logger.Info("running cron job", "app", appname, "args", job.Args, "schedule", job.Schedule)
187
+
go func() {
188
+
if err := command.Run(); err != nil {
189
+
logger.Error("failed to run command", "app", appname, "args", job.Args, "error", err)
190
+
}
191
+
}()
184
192
}
185
193
}
186
194
})
+12
-6
cmd/open.go
+12
-6
cmd/open.go
···
12
12
)
13
13
14
14
func NewCmdOpen() *cobra.Command {
15
+
var flags struct {
16
+
app string
17
+
}
18
+
15
19
cmd := &cobra.Command{
16
-
Use: "open [app]",
17
-
Short: "Open an app in the browser",
18
-
Args: cobra.MaximumNArgs(1),
19
-
ValidArgsFunction: completeApp,
20
+
Use: "open [app]",
21
+
Short: "Open an app in the browser",
22
+
Args: cobra.NoArgs,
20
23
RunE: func(cmd *cobra.Command, args []string) error {
21
-
if len(args) == 0 {
24
+
if flags.app == "" {
22
25
cwd, err := os.Getwd()
23
26
if err != nil {
24
27
return fmt.Errorf("failed to get current directory: %w", err)
···
46
49
return nil
47
50
}
48
51
49
-
a, err := app.LoadApp(args[0], k.String("dir"), k.String("domain"))
52
+
a, err := app.LoadApp(flags.app, k.String("dir"), k.String("domain"))
50
53
if err != nil {
51
54
return fmt.Errorf("failed to load app: %w", err)
52
55
}
···
58
61
return nil
59
62
},
60
63
}
64
+
65
+
cmd.Flags().StringVarP(&flags.app, "app", "a", "", "The app to open")
66
+
cmd.RegisterFlagCompletionFunc("app", completeApp)
61
67
62
68
return cmd
63
69
}
+25
-7
cmd/secrets.go
+25
-7
cmd/secrets.go
···
6
6
"fmt"
7
7
"os"
8
8
"path/filepath"
9
+
"strings"
9
10
10
11
"github.com/cli/go-gh/v2/pkg/tableprinter"
11
12
"github.com/getsops/sops/v3/decrypt"
···
23
24
func NewCmdSecrets() *cobra.Command {
24
25
var flags struct {
25
26
json bool
27
+
app string
26
28
dotenv bool
27
29
}
28
30
29
31
cmd := &cobra.Command{
30
-
Use: "secrets [app]",
31
-
Short: "Print app secrets",
32
-
ValidArgsFunction: completeApp,
33
-
Args: cobra.MaximumNArgs(1),
32
+
Use: "secrets [app]",
33
+
Short: "Print app secrets",
34
+
Args: cobra.NoArgs,
34
35
RunE: func(cmd *cobra.Command, args []string) error {
35
-
if len(args) == 0 {
36
-
return fmt.Errorf("app name is required")
36
+
appName := flags.app
37
+
if appName == "" {
38
+
cwd, err := os.Getwd()
39
+
if err != nil {
40
+
return fmt.Errorf("failed to get current directory: %w", err)
41
+
}
42
+
43
+
if !strings.HasPrefix(cwd, k.String("dir")) {
44
+
return fmt.Errorf("not in an app directory")
45
+
}
46
+
47
+
appDir := cwd
48
+
for filepath.Dir(appDir) != k.String("dir") {
49
+
appDir = filepath.Dir(appDir)
50
+
}
51
+
52
+
appName = filepath.Base(appDir)
37
53
}
38
54
39
-
secretPath := filepath.Join(k.String("dir"), args[0], "secrets.enc.env")
55
+
secretPath := filepath.Join(k.String("dir"), appName, "secrets.enc.env")
40
56
41
57
rawBytes, err := os.ReadFile(secretPath)
42
58
if err != nil {
···
107
123
108
124
cmd.Flags().BoolVar(&flags.json, "json", false, "Output as JSON")
109
125
cmd.Flags().BoolVar(&flags.dotenv, "dotenv", false, "Output as dotenv")
126
+
cmd.Flags().StringVar(&flags.app, "app", "", "App name")
127
+
cmd.RegisterFlagCompletionFunc("app", completeApp)
110
128
111
129
return cmd
112
130
+17
-33
cmd/up.go
+17
-33
cmd/up.go
···
49
49
"github.com/spf13/cobra"
50
50
)
51
51
52
-
var ErrSilent = errors.New("exit error")
53
-
54
52
func NewCmdUp() *cobra.Command {
55
53
var flags struct {
56
54
enableCrons bool
···
66
64
}
67
65
68
66
cmd := &cobra.Command{
69
-
Use: "up",
70
-
Short: "Start the smallweb evaluation server",
71
-
Aliases: []string{"serve"},
72
-
Args: cobra.NoArgs,
73
-
SilenceErrors: true,
67
+
Use: "up",
68
+
Short: "Start the smallweb evaluation server",
69
+
Aliases: []string{"serve"},
70
+
Args: cobra.NoArgs,
74
71
PreRunE: func(cmd *cobra.Command, args []string) error {
75
72
if _, err := checkDenoVersion(); err != nil {
76
73
return err
···
95
92
96
93
if k.String("dir") == "" {
97
94
logger.Error("dir cannot be empty")
98
-
return ErrSilent
95
+
return fmt.Errorf("dir cannot be empty")
99
96
}
100
97
101
98
if k.String("domain") == "" {
102
-
logger.Error("domain cannot be empty")
103
-
return ErrSilent
99
+
return fmt.Errorf("domain cannot be empty")
104
100
}
105
101
106
102
handler := &Handler{
···
138
134
}
139
135
})
140
136
if err != nil {
141
-
logger.Error("failed to create watcher")
142
-
return ErrSilent
137
+
return fmt.Errorf("failed to create watcher: %w", err)
143
138
}
144
139
145
140
handler.watcher = watcher
···
153
148
if flags.tlsCert != "" && flags.tlsKey != "" {
154
149
cert, err := tls.LoadX509KeyPair(flags.tlsCert, flags.tlsKey)
155
150
if err != nil {
156
-
logger.Error("failed to load tls certificate", "error", err)
157
-
return ErrSilent
151
+
return fmt.Errorf("failed to load tls certificate: %w", err)
158
152
}
159
153
160
154
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
···
167
161
168
162
ln, err := getListener(addr, tlsConfig)
169
163
if err != nil {
170
-
logger.Error("failed to get listener", "error", err)
171
-
return ErrSilent
164
+
return fmt.Errorf("failed to get listener: %w", err)
172
165
}
173
166
174
167
logger.Info("serving https", "domain", k.String("domain"), "dir", k.String("dir"), "addr", addr)
···
198
191
199
192
ln, err := getListener(addr, nil)
200
193
if err != nil {
201
-
logger.Error("failed to get listener", "error", err)
202
-
return ErrSilent
194
+
return fmt.Errorf("failed to get listener: %w", err)
203
195
}
204
196
205
197
logger.Info("serving http", "domain", k.String("domain"), "dir", k.String("dir"), "addr", addr)
···
208
200
209
201
if flags.enableCrons {
210
202
logger.Info("starting cron jobs")
211
-
crons := CronRunner(cmd.OutOrStdout(), cmd.ErrOrStderr())
203
+
crons := CronRunner(logger.With("logger", "cron"))
212
204
crons.Start()
213
205
defer crons.Stop()
214
206
}
···
257
249
if flags.sshPrivateKey == "" {
258
250
homeDir, err := os.UserHomeDir()
259
251
if err != nil {
260
-
logger.Error("failed to get home directory", "error", err)
261
-
return ErrSilent
252
+
return fmt.Errorf("failed to get home directory: %w", err)
262
253
}
263
254
264
255
for _, keyType := range []string{"id_rsa", "id_ed25519"} {
···
270
261
}
271
262
272
263
if sshPrivateKeyPath == "" {
273
-
logger.Error("ssh private key not found")
274
-
return ErrSilent
264
+
return fmt.Errorf("ssh private key not found")
275
265
}
276
266
277
267
privateKeyBytes, err := os.ReadFile(sshPrivateKeyPath)
278
268
if err != nil {
279
-
logger.Error("failed to read private key", "error", err)
280
-
return ErrSilent
269
+
return fmt.Errorf("failed to read private key: %w", err)
281
270
}
282
271
283
272
privateKey, err := gossh.ParseRawPrivateKey(privateKeyBytes)
284
273
if err != nil {
285
-
logger.Error("failed to parse private key", "error", err)
286
-
return ErrSilent
274
+
return fmt.Errorf("failed to parse private key: %w", err)
287
275
}
288
276
289
277
signer, err := gossh.NewSignerFromKey(privateKey)
290
278
if err != nil {
291
-
logger.Error("failed to create signer", "error", err)
292
-
return ErrSilent
279
+
return fmt.Errorf("failed to create signer: %w", err)
293
280
}
294
281
295
282
authorizedKey := string(gossh.MarshalAuthorizedKey(signer.PublicKey()))
···
406
393
407
394
if err != nil {
408
395
logger.Error("failed to create ssh server", "error", err)
409
-
return ErrSilent
396
+
return fmt.Errorf("failed to create ssh server: %w", err)
410
397
}
411
398
412
399
logger.Info("serving ssh", "addr", flags.sshAddr)
···
432
419
cmd.Flags().BoolVar(&flags.onDemandTLS, "on-demand-tls", false, "enable on-demand tls")
433
420
cmd.Flags().StringVar(&flags.logFormat, "log-format", "pretty", "log format (json or text)")
434
421
cmd.Flags().BoolVar(&flags.enableCrons, "enable-crons", false, "enable cron jobs")
435
-
cmd.Flags().Bool("cron", false, "enable cron jobs")
436
422
437
-
cmd.Flags().MarkDeprecated("cron", "use --enable-crons instead")
438
-
cmd.Flags().MarkDeprecated("ssh-host-key", "use --ssh-private-key instead")
439
423
cmd.MarkFlagsMutuallyExclusive("on-demand-tls", "tls-cert")
440
424
cmd.MarkFlagsMutuallyExclusive("on-demand-tls", "tls-key")
441
425
cmd.MarkFlagsMutuallyExclusive("on-demand-tls", "addr")
+5
example/cron/main.ts
+5
example/cron/main.ts
+8
example/cron/smallweb.json
+8
example/cron/smallweb.json
-6
main.go
-6
main.go
···
2
2
3
3
import (
4
4
_ "embed"
5
-
"errors"
6
-
"fmt"
7
5
"os"
8
6
9
7
"github.com/pomdtr/smallweb/cmd"
···
17
15
root.SetErr(os.Stderr)
18
16
19
17
if err := root.Execute(); err != nil {
20
-
if !errors.Is(err, cmd.ErrSilent) {
21
-
fmt.Fprintln(os.Stderr, err)
22
-
}
23
-
24
18
os.Exit(1)
25
19
}
26
20
}