Skip to content

Commit b552ebd

Browse files
authored
support cron scheduling (#1695)
* support cron scheduling
1 parent 50663cc commit b552ebd

File tree

12 files changed

+170
-9
lines changed

12 files changed

+170
-9
lines changed

fly-staging.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ kill_signal = 'SIGINT'
99
kill_timeout = '5s'
1010

1111
[env]
12-
HOSTNAME = 'https://next-backend-staging.digger.dev'
12+
DIGGER_HOSTNAME = 'https://next-backend-staging.digger.dev'
1313

1414
[build]
1515
dockerfile = 'Dockerfile_next'

fly.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ kill_signal = 'SIGINT'
99
kill_timeout = '5s'
1010

1111
[env]
12-
HOSTNAME = 'https://next-backend.digger.dev'
12+
DIGGER_HOSTNAME = 'https://next-backend.digger.dev'
1313

1414
[build]
1515
dockerfile = 'Dockerfile_next'

next/controllers/drift.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
package controllers
22

33
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/diggerhq/digger/next/dbmodels"
8+
"github.com/diggerhq/digger/next/utils"
49
"github.com/gin-gonic/gin"
510
"log"
611
"net/http"
12+
"net/url"
13+
"os"
14+
"time"
715
)
816

917
type TriggerDriftRequest struct {
@@ -23,10 +31,78 @@ func (d DiggerController) TriggerDriftDetectionForProject(c *gin.Context) {
2331
}
2432
projectId := request.ProjectId
2533

34+
log.Printf("Drift requests for project: %v", projectId)
35+
2636
c.JSON(200, gin.H{
2737
"status": "successful",
2838
"project_id": projectId,
2939
})
3040
return
3141

3242
}
43+
44+
func (d DiggerController) TriggerCronForMatchingProjects(c *gin.Context) {
45+
webhookSecret := os.Getenv("DIGGER_WEBHOOK_SECRET")
46+
diggerHostName := os.Getenv("DIGGER_HOSTNAME")
47+
48+
driftUrl, err := url.JoinPath(diggerHostName, "_internal/trigger_drift")
49+
if err != nil {
50+
log.Printf("could not form drift url: %v", err)
51+
c.JSON(500, gin.H{"error": "could not form drift url"})
52+
return
53+
}
54+
55+
p := dbmodels.DB.Query.Project
56+
driftEnabledProjects, err := dbmodels.DB.Query.Project.Where(p.IsDriftDetectionEnabled.Is(true)).Find()
57+
if err != nil {
58+
log.Printf("could not fetch drift enabled projects: %v", err)
59+
c.JSON(500, gin.H{"error": "could not fetch drift enabled projects"})
60+
return
61+
}
62+
63+
for _, proj := range driftEnabledProjects {
64+
matches, err := utils.MatchesCrontab(proj.DriftCrontab, time.Now())
65+
if err != nil {
66+
log.Printf("could not check for matching crontab, %v", err)
67+
// TODO: send metrics here
68+
continue
69+
}
70+
71+
if matches {
72+
payload := TriggerDriftRequest{ProjectId: proj.ID}
73+
74+
// Convert payload to JSON
75+
jsonPayload, err := json.Marshal(payload)
76+
if err != nil {
77+
fmt.Println("Error marshaling JSON:", err)
78+
return
79+
}
80+
81+
// Create a new request
82+
req, err := http.NewRequest("POST", driftUrl, bytes.NewBuffer(jsonPayload))
83+
if err != nil {
84+
fmt.Println("Error creating request:", err)
85+
return
86+
}
87+
88+
// Set headers
89+
req.Header.Set("Content-Type", "application/json")
90+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", webhookSecret))
91+
92+
// Send the request
93+
client := &http.Client{}
94+
resp, err := client.Do(req)
95+
if err != nil {
96+
fmt.Println("Error sending request:", err)
97+
return
98+
}
99+
defer resp.Body.Close()
100+
101+
// Get the status code
102+
statusCode := resp.StatusCode
103+
if statusCode != 200 {
104+
log.Printf("got unexpected drift status for project: %v - status: %v", proj.ID, statusCode)
105+
}
106+
}
107+
}
108+
}

next/controllers/github.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func GithubAppSetup(c *gin.Context) {
119119
Webhook *githubWebhook `json:"hook_attributes"`
120120
}
121121

122-
host := os.Getenv("HOSTNAME")
122+
host := os.Getenv("DIGGER_HOSTNAME")
123123
manifest := &githubAppRequest{
124124
Name: fmt.Sprintf("Digger app %v", rand.Int31()),
125125
Description: fmt.Sprintf("Digger hosted at %s", host),
@@ -533,7 +533,7 @@ func ConvertJobsToDiggerJobs(jobType orchestrator_scheduler.DiggerCommand, vcsTy
533533
}
534534
organisationName := organisation.Title
535535

536-
backendHostName := os.Getenv("HOSTNAME")
536+
backendHostName := os.Getenv("DIGGER_HOSTNAME")
537537

538538
log.Printf("Number of Jobs: %v\n", len(jobsMap))
539539
marshalledJobsMap := map[string][]byte{}

next/controllers/github_after_merge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func handlePushEventApplyAfterMerge(gh nextutils.GithubClientProvider, payload *
2424
requestedBy := *payload.Sender.Login
2525
ref := *payload.Ref
2626
targetBranch := strings.ReplaceAll(ref, "refs/heads/", "")
27-
backendHostName := os.Getenv("HOSTNAME")
27+
backendHostName := os.Getenv("DIGGER_HOSTNAME")
2828

2929
link, err := dbmodels.DB.GetGithubAppInstallationLink(installationId)
3030
if err != nil {

next/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ require (
1717
github.com/google/go-github/v61 v61.0.0
1818
github.com/google/uuid v1.6.0
1919
github.com/orandin/slog-gorm v1.3.2
20+
github.com/robfig/cron/v3 v3.0.1
2021
github.com/samber/lo v1.46.0
2122
github.com/samber/slog-gin v1.13.3
22-
github.com/stretchr/testify v1.9.0
2323
github.com/supabase-community/supabase-go v0.0.4
2424
golang.org/x/oauth2 v0.22.0
2525
gorm.io/driver/postgres v1.5.9
@@ -105,7 +105,6 @@ require (
105105
github.com/creack/pty v1.1.17 // indirect
106106
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
107107
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
108-
github.com/diggerhq/digger/cli v0.0.0-20240705091808-75187a7aae8e // indirect
109108
github.com/dimchansky/utfbom v1.1.1 // indirect
110109
github.com/dineshba/tf-summarize v0.3.10 // indirect
111110
github.com/emirpasic/gods v1.18.1 // indirect
@@ -246,6 +245,7 @@ require (
246245
github.com/spf13/cast v1.6.0 // indirect
247246
github.com/spf13/pflag v1.0.5 // indirect
248247
github.com/spf13/viper v1.18.2 // indirect
248+
github.com/stretchr/testify v1.9.0 // indirect
249249
github.com/subosito/gotenv v1.6.0 // indirect
250250
github.com/supabase-community/functions-go v0.0.0-20220927045802-22373e6cb51d // indirect
251251
github.com/supabase-community/gotrue-go v1.2.0 // indirect

next/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,8 +461,6 @@ github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkz
461461
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
462462
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
463463
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
464-
github.com/diggerhq/digger/cli v0.0.0-20240705091808-75187a7aae8e h1:aRBJ92ZbJc6VQXx6zPihHuQoDotstDTwUi3C8gdbdgw=
465-
github.com/diggerhq/digger/cli v0.0.0-20240705091808-75187a7aae8e/go.mod h1:+UUif/7rqA5ElbNiYXyu6adjpXcafe5nSrY+IvFoJVA=
466464
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
467465
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
468466
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
@@ -1137,6 +1135,8 @@ github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo
11371135
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
11381136
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
11391137
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
1138+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
1139+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
11401140
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
11411141
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
11421142
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

next/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func main() {
7777
r.POST("/github-app-webhook", diggerController.GithubAppWebHook)
7878

7979
r.POST("/_internal/process_runs_queue", middleware.WebhookAuth(), diggerController.ProcessRunQueueItems)
80+
// process all drift crontabs
81+
r.POST("/_internal/process_drift", middleware.WebhookAuth(), diggerController.TriggerCronForMatchingProjects)
82+
// trigger for specific project
8083
r.POST("/_internal/trigger_drift", middleware.WebhookAuth(), diggerController.TriggerDriftDetectionForProject)
8184
//authorized := r.Group("/")
8285
//authorized.Use(middleware.GetApiMiddleware(), middleware.AccessLevel(dbmodels.CliJobAccessType, dbmodels.AccessPolicyType, models.AdminPolicyType))

next/scripts/cron/process_drift.query

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
select
2+
cron.schedule(
3+
'invoke-function-every-half-minute',
4+
'30 seconds',
5+
$$
6+
select
7+
net.http_post(
8+
url:='https://{DIGGER_HOSTNAME}/_internal/process_drift',
9+
headers:=jsonb_build_object('Content-Type','application/json', 'Authorization', 'Bearer ' || 'abc123'),
10+
body:=jsonb_build_object('time', now() ),
11+
timeout_milliseconds:=5000
12+
) as request_id;
13+
$$
14+
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
select
2+
cron.schedule(
3+
'invoke-function-every-half-minute',
4+
'30 seconds',
5+
$$
6+
select
7+
net.http_post(
8+
url:='https://{DIGGER_HOSTNAME}/_internal/process_runs_queue',
9+
headers:='{"Content-Type": "application/json", "Authorization": "Bearer abc123"}'::jsonb,
10+
body:='{}'::jsonb
11+
) as request_id;
12+
$$
13+
);
14+
15+

next/utils/crontab.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"github.com/robfig/cron/v3"
6+
"time"
7+
)
8+
9+
func MatchesCrontab(cronString string, timestamp time.Time) (bool, error) {
10+
// Parse the crontab string
11+
schedule, err := cron.ParseStandard(cronString)
12+
if err != nil {
13+
return false, fmt.Errorf("failed to parse crontab string: %w", err)
14+
}
15+
16+
// Round down the timestamp to the nearest minute
17+
roundedTime := timestamp.Truncate(time.Minute)
18+
19+
// Check if the rounded time matches the schedule
20+
nextTime := schedule.Next(roundedTime.Add(-time.Minute))
21+
return nextTime.Equal(roundedTime), nil
22+
}

next/utils/crontab_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"github.com/stretchr/testify/assert"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestCrontTabMatching(t *testing.T) {
11+
cronString := "*/15 * * * *" // Every 15 minutes
12+
timestamp := time.Date(2023, 5, 1, 12, 30, 30, 0, time.UTC)
13+
14+
matches, err := MatchesCrontab(cronString, timestamp)
15+
if err != nil {
16+
fmt.Printf("Error: %v\n", err)
17+
return
18+
}
19+
assert.True(t, matches)
20+
21+
cronString = "*/15 * * * *" // Every 15 minutes
22+
timestamp = time.Date(2022, 5, 1, 12, 12, 30, 0, time.UTC)
23+
24+
matches, err = MatchesCrontab(cronString, timestamp)
25+
if err != nil {
26+
fmt.Printf("Error: %v\n", err)
27+
return
28+
}
29+
assert.False(t, matches)
30+
31+
}

0 commit comments

Comments
 (0)