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