Skip to content

Commit faab5fd

Browse files
committed
cron: refactor into interfaces
Refactor 'internal/cron.go' into a 'CronScheduler' interface, and 'cmd/git-bundle-server/cron.go' (moved to 'cmd/utils/cron.go') into a 'CronHelper' interface. The former converts standalone functions previously in the file into struct methods, while also moving much of the job addition/update logic from 'cmd/git-bundle-server/cron.go' into a new 'AddJob()'. The 'CronHelper' has one method - 'SetCronSchedule()' - which calls 'AddJob()' to set the daily 'update-all' schedule (used by the 'init' and 'start' commands). Both of these structs utilize the existing helper structures ('FileSystem', 'CommandExecutor', and 'UserProvider') to handle all system interaction. As a result, they can both be unit tested in a future update. Signed-off-by: Victoria Dye <[email protected]>
1 parent f6a7208 commit faab5fd

File tree

6 files changed

+151
-91
lines changed

6 files changed

+151
-91
lines changed

cmd/git-bundle-server/cron.go

-60
This file was deleted.

cmd/git-bundle-server/init.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ func (i *initCmd) Run(ctx context.Context, args []string) error {
8383
return i.logger.Errorf(ctx, "failed to write bundle list: %w", listErr)
8484
}
8585

86-
SetCronSchedule()
86+
cron := utils.GetDependency[utils.CronHelper](ctx, i.container)
87+
cron.SetCronSchedule(ctx)
8788

8889
return nil
8990
}

cmd/git-bundle-server/start.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ func (s *startCmd) Run(ctx context.Context, args []string) error {
5151
}
5252

5353
// Make sure we have the global schedule running.
54-
SetCronSchedule()
54+
cron := utils.GetDependency[utils.CronHelper](ctx, s.container)
55+
cron.SetCronSchedule(ctx)
5556

5657
return nil
5758
}

cmd/utils/container-helpers.go

+15
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ func BuildGitBundleServerContainer(logger log.TraceLogger) *DependencyContainer
3232
registerDependency(container, func(ctx context.Context) bundles.BundleProvider {
3333
return bundles.NewBundleProvider(logger)
3434
})
35+
registerDependency(container, func(ctx context.Context) core.CronScheduler {
36+
return core.NewCronScheduler(
37+
logger,
38+
GetDependency[common.UserProvider](ctx, container),
39+
GetDependency[cmd.CommandExecutor](ctx, container),
40+
GetDependency[common.FileSystem](ctx, container),
41+
)
42+
})
43+
registerDependency(container, func(ctx context.Context) CronHelper {
44+
return NewCronHelper(
45+
logger,
46+
GetDependency[common.FileSystem](ctx, container),
47+
GetDependency[core.CronScheduler](ctx, container),
48+
)
49+
})
3550
registerDependency(container, func(ctx context.Context) daemon.DaemonProvider {
3651
t, err := daemon.NewDaemonProvider(
3752
logger,

cmd/utils/cron.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
6+
"github.com/github/git-bundle-server/internal/common"
7+
"github.com/github/git-bundle-server/internal/core"
8+
"github.com/github/git-bundle-server/internal/log"
9+
)
10+
11+
type CronHelper interface {
12+
SetCronSchedule(ctx context.Context) error
13+
}
14+
15+
type cronHelper struct {
16+
logger log.TraceLogger
17+
fileSystem common.FileSystem
18+
scheduler core.CronScheduler
19+
}
20+
21+
func NewCronHelper(
22+
l log.TraceLogger,
23+
fs common.FileSystem,
24+
s core.CronScheduler,
25+
) CronHelper {
26+
return &cronHelper{
27+
logger: l,
28+
fileSystem: fs,
29+
scheduler: s,
30+
}
31+
}
32+
33+
func (c *cronHelper) SetCronSchedule(ctx context.Context) error {
34+
pathToExec, err := c.fileSystem.GetLocalExecutable("git-bundle-server")
35+
if err != nil {
36+
return c.logger.Errorf(ctx, "failed to get executable: %w", err)
37+
}
38+
39+
err = c.scheduler.AddJob(ctx, core.CronDaily, pathToExec, []string{"update-all"})
40+
if err != nil {
41+
return c.logger.Errorf(ctx, "failed to set cron schedule: %w", err)
42+
}
43+
44+
return nil
45+
}

internal/core/cron.go

+87-29
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,120 @@ package core
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
6-
"os/exec"
7+
"strings"
8+
9+
"github.com/github/git-bundle-server/internal/cmd"
10+
"github.com/github/git-bundle-server/internal/common"
11+
"github.com/github/git-bundle-server/internal/log"
12+
"github.com/github/git-bundle-server/internal/utils"
13+
)
14+
15+
type cronSchedule string
16+
17+
const (
18+
CronDaily cronSchedule = "0 0 * * *"
19+
CronWeekly cronSchedule = "0 0 0 * *"
720
)
821

9-
func GetCrontabCommand(args ...string) (*exec.Cmd, error) {
10-
crontab, err := exec.LookPath("crontab")
22+
type CronScheduler interface {
23+
AddJob(ctx context.Context, schedule cronSchedule,
24+
exePath string, args []string) error
25+
}
26+
27+
type cronScheduler struct {
28+
logger log.TraceLogger
29+
user common.UserProvider
30+
cmdExec cmd.CommandExecutor
31+
fileSystem common.FileSystem
32+
}
33+
34+
func NewCronScheduler(
35+
l log.TraceLogger,
36+
u common.UserProvider,
37+
c cmd.CommandExecutor,
38+
fs common.FileSystem,
39+
) CronScheduler {
40+
return &cronScheduler{
41+
logger: l,
42+
user: u,
43+
cmdExec: c,
44+
fileSystem: fs,
45+
}
46+
}
47+
48+
func (c *cronScheduler) loadExistingSchedule(ctx context.Context) ([]byte, error) {
49+
buffer := bytes.Buffer{}
50+
exitCode, err := c.cmdExec.Run(ctx, "crontab", []string{"-l"}, cmd.Stdout(&buffer))
1151
if err != nil {
12-
return nil, fmt.Errorf("failed to find 'crontab' on the path: %w", err)
52+
return nil, c.logger.Error(ctx, err)
53+
} else if exitCode != 0 {
54+
return nil, c.logger.Errorf(ctx, "'crontab' exited with status %d", exitCode)
1355
}
1456

15-
cmd := exec.Command(crontab, args...)
16-
return cmd, nil
57+
return buffer.Bytes(), nil
1758
}
1859

19-
func LoadExistingSchedule() ([]byte, error) {
20-
cmd, err := GetCrontabCommand("-l")
60+
func (c *cronScheduler) commitCronSchedule(ctx context.Context, filename string) error {
61+
exitCode, err := c.cmdExec.RunQuiet(ctx, "crontab", filename)
2162
if err != nil {
22-
return nil, err
63+
return c.logger.Error(ctx, err)
64+
} else if exitCode != 0 {
65+
return c.logger.Errorf(ctx, "'crontab' exited with status %d", exitCode)
2366
}
2467

25-
buffer := bytes.Buffer{}
26-
cmd.Stdout = &buffer
68+
return nil
69+
}
2770

28-
errorBuffer := bytes.Buffer{}
29-
cmd.Stderr = &errorBuffer
71+
func (c *cronScheduler) AddJob(ctx context.Context,
72+
schedule cronSchedule,
73+
exePath string,
74+
args []string,
75+
) error {
76+
newSchedule := fmt.Sprintf("%s \"%s\" %s",
77+
schedule,
78+
exePath,
79+
utils.Map(args, func(s string) string { return "\"" + s + "\"" }),
80+
)
3081

31-
err = cmd.Start()
82+
scheduleBytes, err := c.loadExistingSchedule(ctx)
3283
if err != nil {
33-
return nil, fmt.Errorf("crontab failed to start: %w", err)
84+
return c.logger.Errorf(ctx, "failed to get existing cron schedule: %w", err)
3485
}
3586

36-
err = cmd.Wait()
37-
if err != nil {
38-
return nil, fmt.Errorf("crontab returned a failure: %w\nstderr: %s", err, errorBuffer.String())
87+
scheduleStr := string(scheduleBytes)
88+
89+
// TODO: Use comments to indicate a "region" where our schedule
90+
// is set, so we can remove the entire region even if we update
91+
// the schedule in the future.
92+
if strings.Contains(scheduleStr, newSchedule) {
93+
// We already have this schedule, so skip modifying
94+
// the crontab schedule.
95+
return nil
3996
}
4097

41-
return buffer.Bytes(), nil
42-
}
98+
scheduleBytes = append(scheduleBytes, []byte(schedule)...)
4399

44-
func CommitCronSchedule(filename string) error {
45-
cmd, err := GetCrontabCommand(filename)
100+
user, err := c.user.CurrentUser()
46101
if err != nil {
47-
return err
102+
return c.logger.Error(ctx, err)
48103
}
104+
scheduleFile := CrontabFile(user)
49105

50-
errorBuffer := bytes.Buffer{}
51-
cmd.Stderr = &errorBuffer
106+
err = c.fileSystem.WriteFile(scheduleFile, scheduleBytes)
107+
if err != nil {
108+
return c.logger.Errorf(ctx, "failed to write new cron schedule to temp file: %w", err)
109+
}
52110

53-
err = cmd.Start()
111+
err = c.commitCronSchedule(ctx, scheduleFile)
54112
if err != nil {
55-
return fmt.Errorf("crontab failed to start: %w", err)
113+
return c.logger.Errorf(ctx, "failed to commit new cron schedule: %w", err)
56114
}
57115

58-
err = cmd.Wait()
116+
_, err = c.fileSystem.DeleteFile(scheduleFile)
59117
if err != nil {
60-
return fmt.Errorf("crontab returned a failure: %w\nstderr: %s", err, errorBuffer.String())
118+
return c.logger.Errorf(ctx, "failed to clear schedule temp file: %w", err)
61119
}
62120

63121
return nil

0 commit comments

Comments
 (0)