Skip to content

Commit c717f1f

Browse files
committed
Merge branch 'release/v1.14.0'
2 parents 650d13a + 78ba704 commit c717f1f

File tree

119 files changed

+12179
-2111
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+12179
-2111
lines changed

backend/go-test-exit.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0

backend/go-test.log

Lines changed: 3738 additions & 0 deletions
Large diffs are not rendered by default.

backend/go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ require (
1717
github.com/gin-contrib/cors v1.7.6
1818
github.com/gin-gonic/gin v1.11.0
1919
github.com/glebarez/sqlite v1.11.0
20-
github.com/go-co-op/gocron/v2 v2.19.0
2120
github.com/go-git/go-git/v5 v5.16.4
2221
github.com/goccy/go-yaml v1.19.2
2322
github.com/gofrs/flock v0.13.0
@@ -31,6 +30,7 @@ require (
3130
github.com/lmittmann/tint v1.1.2
3231
github.com/nicholas-fedor/shoutrrr v0.13.1
3332
github.com/orandin/slog-gorm v1.4.0
33+
github.com/robfig/cron/v3 v3.0.1
3434
github.com/samber/slog-gin v1.19.1
3535
github.com/shirou/gopsutil/v4 v4.25.12
3636
github.com/spf13/cobra v1.10.2
@@ -172,7 +172,6 @@ require (
172172
github.com/quic-go/qpack v0.6.0 // indirect
173173
github.com/quic-go/quic-go v0.57.0 // indirect
174174
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
175-
github.com/robfig/cron/v3 v3.0.1 // indirect
176175
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
177176
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
178177
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect

backend/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,6 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
184184
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
185185
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
186186
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
187-
github.com/go-co-op/gocron/v2 v2.19.0 h1:OKf2y6LXPs/BgBI2fl8PxUpNAI1DA9Mg+hSeGOS38OU=
188-
github.com/go-co-op/gocron/v2 v2.19.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
189187
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
190188
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
191189
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=

backend/internal/bootstrap/bootstrap.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import (
1818
"github.com/getarcaneapp/arcane/backend/internal/config"
1919
"github.com/getarcaneapp/arcane/backend/internal/utils"
2020
"github.com/getarcaneapp/arcane/backend/internal/utils/crypto"
21+
"github.com/getarcaneapp/arcane/backend/internal/utils/edge"
2122
httputils "github.com/getarcaneapp/arcane/backend/internal/utils/http"
23+
"github.com/getarcaneapp/arcane/backend/pkg/scheduler"
2224
)
2325

2426
func Bootstrap(ctx context.Context) error {
@@ -59,6 +61,19 @@ func Bootstrap(ctx context.Context) error {
5961
utils.EnsureEncryptionKey(appCtx, cfg, appServices.Settings.EnsureEncryptionKey)
6062
crypto.InitEncryption(cfg)
6163
utils.InitializeDefaultSettings(appCtx, cfg, appServices.Settings)
64+
utils.MigrateSchedulerCronValues(
65+
appCtx,
66+
appServices.Settings.GetStringSetting,
67+
appServices.Settings.UpdateSetting,
68+
appServices.Settings.LoadDatabaseSettings,
69+
)
70+
if appServices.GitOpsSync != nil {
71+
utils.MigrateGitOpsSyncIntervals(
72+
appCtx,
73+
appServices.GitOpsSync.ListSyncIntervalsRaw,
74+
appServices.GitOpsSync.UpdateSyncIntervalMinutes,
75+
)
76+
}
6277

6378
if err := appServices.Settings.NormalizeProjectsDirectory(appCtx, cfg.ProjectsDirectory); err != nil {
6479
slog.WarnContext(appCtx, "Failed to normalize projects directory", "error", err)
@@ -90,15 +105,29 @@ func Bootstrap(ctx context.Context) error {
90105
}
91106
}
92107

93-
scheduler, err := initializeScheduler()
94-
if err != nil {
95-
return fmt.Errorf("failed to create job scheduler: %w", err)
96-
}
108+
scheduler := scheduler.NewJobScheduler(appCtx)
109+
appServices.JobSchedule.SetScheduler(scheduler)
97110
registerJobs(appCtx, scheduler, appServices, cfg)
98111

99-
router := setupRouter(cfg, appServices) //nolint:contextcheck
112+
router, tunnelServer := setupRouter(appCtx, cfg, appServices)
113+
114+
// Start edge tunnel client if running as an edge agent
115+
if cfg.EdgeAgent && cfg.ManagerApiUrl != "" && cfg.AgentToken != "" {
116+
slog.InfoContext(appCtx, "Starting edge tunnel client", "manager_url", cfg.ManagerApiUrl)
117+
errCh, err := edge.StartTunnelClientWithErrors(appCtx, cfg, router)
118+
if err != nil {
119+
slog.ErrorContext(appCtx, "Failed to start edge tunnel client", "error", err)
120+
} else {
121+
slog.InfoContext(appCtx, "Edge tunnel client started", "manager_url", cfg.ManagerApiUrl)
122+
go func() {
123+
for err := range errCh {
124+
slog.ErrorContext(appCtx, "Edge tunnel client error", "error", err)
125+
}
126+
}()
127+
}
128+
}
100129

101-
err = runServices(appCtx, cfg, router, scheduler)
130+
err = runServices(appCtx, cfg, router, tunnelServer, scheduler)
102131
if err != nil {
103132
return fmt.Errorf("failed to run services: %w", err)
104133
}
@@ -151,16 +180,19 @@ func handleAgentBootstrapPairing(ctx context.Context, cfg *config.Config, httpCl
151180
}
152181
}
153182

154-
func runServices(appCtx context.Context, cfg *config.Config, router http.Handler, scheduler interface{ Run(context.Context) error }) error {
155-
go func() {
156-
slog.InfoContext(appCtx, "Starting scheduler")
157-
if err := scheduler.Run(appCtx); err != nil {
158-
if !errors.Is(err, context.Canceled) {
159-
slog.ErrorContext(appCtx, "Job scheduler exited with error", "error", err)
183+
func runServices(appCtx context.Context, cfg *config.Config, router http.Handler, tunnelServer *edge.TunnelServer, schedulers ...interface{ Run(context.Context) error }) error {
184+
for _, s := range schedulers {
185+
scheduler := s
186+
go func() {
187+
slog.InfoContext(appCtx, "Starting scheduler")
188+
if err := scheduler.Run(appCtx); err != nil {
189+
if !errors.Is(err, context.Canceled) {
190+
slog.ErrorContext(appCtx, "Job scheduler exited with error", "error", err)
191+
}
160192
}
161-
}
162-
slog.InfoContext(appCtx, "Scheduler stopped")
163-
}()
193+
slog.InfoContext(appCtx, "Scheduler stopped")
194+
}()
195+
}
164196

165197
srv := &http.Server{
166198
Addr: ":" + cfg.Port,
@@ -194,6 +226,11 @@ func runServices(appCtx context.Context, cfg *config.Config, router http.Handler
194226
return err
195227
}
196228

229+
// Wait for tunnel cleanup loop to finish
230+
if tunnelServer != nil {
231+
tunnelServer.WaitForCleanupDone()
232+
}
233+
197234
slog.InfoContext(shutdownCtx, "Server stopped gracefully") //nolint:contextcheck
198235
return nil
199236
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package bootstrap
2+
3+
import (
4+
"context"
5+
"errors"
6+
"log/slog"
7+
8+
"github.com/getarcaneapp/arcane/backend/internal/models"
9+
"github.com/getarcaneapp/arcane/backend/internal/utils/edge"
10+
"github.com/gin-gonic/gin"
11+
)
12+
13+
// registerEdgeTunnelRoutes registers the edge tunnel WebSocket endpoint on the manager.
14+
// This allows edge agents to connect and establish a tunnel for proxied requests.
15+
// Returns the TunnelServer for graceful shutdown.
16+
func registerEdgeTunnelRoutes(ctx context.Context, apiGroup *gin.RouterGroup, appServices *Services) *edge.TunnelServer {
17+
// Resolver that validates API key and returns the environment ID
18+
resolver := func(ctx context.Context, token string) (string, error) {
19+
// Use the ApiKeyService which properly validates the key hash
20+
envID, err := appServices.ApiKey.GetEnvironmentByApiKey(ctx, token)
21+
if err != nil {
22+
return "", err
23+
}
24+
if envID == nil {
25+
return "", errors.New("API key is not linked to an environment")
26+
}
27+
return *envID, nil
28+
}
29+
30+
// Status callback to update environment status when agent connects/disconnects
31+
statusCallback := func(ctx context.Context, envID string, connected bool) {
32+
var status string
33+
if connected {
34+
status = string(models.EnvironmentStatusOnline)
35+
// Update heartbeat when connecting
36+
if err := appServices.Environment.UpdateEnvironmentHeartbeat(ctx, envID); err != nil {
37+
slog.WarnContext(ctx, "Failed to update heartbeat on edge connect", "environment_id", envID, "error", err)
38+
}
39+
} else {
40+
status = string(models.EnvironmentStatusOffline)
41+
}
42+
43+
updates := map[string]interface{}{
44+
"status": status,
45+
}
46+
_, err := appServices.Environment.UpdateEnvironment(ctx, envID, updates, nil, nil)
47+
if err != nil {
48+
slog.WarnContext(ctx, "Failed to update environment status on edge connect/disconnect", "environment_id", envID, "connected", connected, "error", err)
49+
} else {
50+
slog.InfoContext(ctx, "Updated environment status", "environment_id", envID, "status", status)
51+
}
52+
}
53+
54+
return edge.RegisterTunnelRoutes(ctx, apiGroup, resolver, statusCallback)
55+
}

backend/internal/bootstrap/jobs_bootstrap.go

Lines changed: 44 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,88 @@ package bootstrap
22

33
import (
44
"context"
5-
"fmt"
65
"log/slog"
76

87
"github.com/getarcaneapp/arcane/backend/internal/config"
9-
"github.com/getarcaneapp/arcane/backend/internal/job"
8+
pkg_scheduler "github.com/getarcaneapp/arcane/backend/pkg/scheduler"
109
)
1110

12-
func initializeScheduler() (*job.Scheduler, error) {
13-
scheduler, err := job.NewScheduler()
14-
if err != nil {
15-
return nil, fmt.Errorf("failed to create job scheduler: %w", err)
16-
}
17-
return scheduler, nil
18-
}
11+
func registerJobs(appCtx context.Context, newScheduler *pkg_scheduler.JobScheduler, appServices *Services, appConfig *config.Config) {
12+
autoUpdateJob := pkg_scheduler.NewAutoUpdateJob(appServices.Updater, appServices.Settings)
13+
newScheduler.RegisterJob(autoUpdateJob)
1914

20-
func registerJobs(appCtx context.Context, scheduler *job.Scheduler, appServices *Services, appConfig *config.Config) {
21-
autoUpdateJob := job.NewAutoUpdateJob(scheduler, appServices.Updater, appServices.Settings)
22-
if err := autoUpdateJob.Register(appCtx); err != nil {
23-
slog.ErrorContext(appCtx, "Failed to register auto-update job", "error", err)
24-
}
15+
imagePollingJob := pkg_scheduler.NewImagePollingJob(appServices.ImageUpdate, appServices.Settings, appServices.Environment)
16+
newScheduler.RegisterJob(imagePollingJob)
2517

26-
imagePollingJob := job.NewImagePollingJob(scheduler, appServices.ImageUpdate, appServices.Settings, appServices.Environment)
27-
if err := imagePollingJob.Register(appCtx); err != nil {
28-
slog.ErrorContext(appCtx, "Failed to register image polling job", "error", err)
29-
}
30-
31-
environmentHealthJob := job.NewEnvironmentHealthJob(scheduler, appServices.Environment, appServices.Settings)
18+
environmentHealthJob := pkg_scheduler.NewEnvironmentHealthJob(appServices.Environment, appServices.Settings)
3219
if !appConfig.AgentMode {
33-
if err := environmentHealthJob.Register(appCtx); err != nil {
34-
slog.ErrorContext(appCtx, "Failed to register environment health check job", "error", err)
35-
}
20+
newScheduler.RegisterJob(environmentHealthJob)
3621
}
3722

38-
analyticsJob := job.NewAnalyticsJob(scheduler, appServices.Settings, nil, appConfig)
39-
if err := analyticsJob.Register(appCtx); err != nil {
40-
slog.ErrorContext(appCtx, "Failed to register analytics heartbeat job", "error", err)
41-
}
23+
analyticsJob := pkg_scheduler.NewAnalyticsJob(appServices.Settings, nil, appConfig)
24+
newScheduler.RegisterJob(analyticsJob)
4225

43-
eventCleanupJob := job.NewEventCleanupJob(scheduler, appServices.Event, appServices.Settings)
44-
if err := eventCleanupJob.Register(appCtx); err != nil {
45-
slog.ErrorContext(appCtx, "Failed to register event cleanup job", "error", err)
46-
}
26+
eventCleanupJob := pkg_scheduler.NewEventCleanupJob(appServices.Event, appServices.Settings)
27+
newScheduler.RegisterJob(eventCleanupJob)
4728

48-
scheduledPruneJob := job.NewScheduledPruneJob(scheduler, appServices.System, appServices.Settings)
49-
if err := scheduledPruneJob.Register(appCtx); err != nil {
50-
slog.ErrorContext(appCtx, "Failed to register scheduled prune job", "error", err)
51-
}
29+
scheduledPruneJob := pkg_scheduler.NewScheduledPruneJob(appServices.System, appServices.Settings)
30+
newScheduler.RegisterJob(scheduledPruneJob)
5231

53-
fsWatcherJob, err := job.RegisterFilesystemWatcherJob(appCtx, scheduler, appServices.Project, appServices.Template, appServices.Settings)
32+
fsWatcherJob, err := pkg_scheduler.RegisterFilesystemWatcherJob(appCtx, appServices.Project, appServices.Template, appServices.Settings)
5433
if err != nil {
5534
slog.ErrorContext(appCtx, "Failed to register filesystem watcher job", "error", err)
5635
}
5736

58-
gitOpsSyncJob := job.NewGitOpsSyncJob(scheduler, appServices.GitOpsSync, appServices.Settings)
59-
if err := gitOpsSyncJob.Register(appCtx); err != nil {
60-
slog.ErrorContext(appCtx, "Failed to register GitOps sync job", slog.Any("error", err))
61-
}
37+
gitOpsSyncJob := pkg_scheduler.NewGitOpsSyncJob(appServices.GitOpsSync, appServices.Settings)
38+
newScheduler.RegisterJob(gitOpsSyncJob)
6239

63-
setupJobScheduleCallbacks(appServices, appConfig, environmentHealthJob, analyticsJob, eventCleanupJob)
64-
setupSettingsCallbacks(appServices, appConfig, imagePollingJob, autoUpdateJob, environmentHealthJob, fsWatcherJob, scheduledPruneJob)
40+
setupJobScheduleCallbacks(appServices, appConfig, newScheduler, environmentHealthJob, analyticsJob, eventCleanupJob)
41+
setupSettingsCallbacks(appServices, appConfig, newScheduler, imagePollingJob, autoUpdateJob, environmentHealthJob, fsWatcherJob, scheduledPruneJob)
6542
}
6643

67-
func setupJobScheduleCallbacks(appServices *Services, appConfig *config.Config, environmentHealthJob *job.EnvironmentHealthJob, analyticsJob *job.AnalyticsJob, eventCleanupJob *job.EventCleanupJob) {
44+
func setupJobScheduleCallbacks(appServices *Services, appConfig *config.Config, newScheduler *pkg_scheduler.JobScheduler, environmentHealthJob *pkg_scheduler.EnvironmentHealthJob, analyticsJob *pkg_scheduler.AnalyticsJob, eventCleanupJob *pkg_scheduler.EventCleanupJob) {
6845
if appServices.JobSchedule != nil {
69-
appServices.JobSchedule.OnJobSchedulesChanged = func(ctx context.Context) {
70-
if !appConfig.AgentMode {
71-
if err := environmentHealthJob.Reschedule(ctx); err != nil {
72-
slog.WarnContext(ctx, "Failed to reschedule environment-health job", "error", err)
46+
appServices.JobSchedule.OnJobSchedulesChanged = func(ctx context.Context, changedKeys []string) {
47+
for _, key := range changedKeys {
48+
switch key {
49+
case "environmentHealthInterval":
50+
if appConfig.AgentMode {
51+
continue
52+
}
53+
if err := newScheduler.RescheduleJob(ctx, environmentHealthJob); err != nil {
54+
slog.WarnContext(ctx, "Failed to reschedule environment-health job", "error", err)
55+
}
56+
case "analyticsHeartbeatInterval":
57+
if err := newScheduler.RescheduleJob(ctx, analyticsJob); err != nil {
58+
slog.WarnContext(ctx, "Failed to reschedule analytics heartbeat job", "error", err)
59+
}
60+
case "eventCleanupInterval":
61+
if err := newScheduler.RescheduleJob(ctx, eventCleanupJob); err != nil {
62+
slog.WarnContext(ctx, "Failed to reschedule event cleanup job", "error", err)
63+
}
7364
}
7465
}
75-
if err := analyticsJob.Reschedule(ctx); err != nil {
76-
slog.WarnContext(ctx, "Failed to reschedule analytics heartbeat job", "error", err)
77-
}
78-
if err := eventCleanupJob.Reschedule(ctx); err != nil {
79-
slog.WarnContext(ctx, "Failed to reschedule event cleanup job", "error", err)
80-
}
8166
}
8267
}
8368
}
8469

85-
func setupSettingsCallbacks(appServices *Services, appConfig *config.Config, imagePollingJob *job.ImagePollingJob, autoUpdateJob *job.AutoUpdateJob, environmentHealthJob *job.EnvironmentHealthJob, fsWatcherJob *job.FilesystemWatcherJob, scheduledPruneJob *job.ScheduledPruneJob) {
70+
func setupSettingsCallbacks(appServices *Services, appConfig *config.Config, newScheduler *pkg_scheduler.JobScheduler, imagePollingJob *pkg_scheduler.ImagePollingJob, autoUpdateJob *pkg_scheduler.AutoUpdateJob, environmentHealthJob *pkg_scheduler.EnvironmentHealthJob, fsWatcherJob *pkg_scheduler.FilesystemWatcherJob, scheduledPruneJob *pkg_scheduler.ScheduledPruneJob) {
8671
appServices.Settings.OnImagePollingSettingsChanged = func(ctx context.Context) {
87-
if err := imagePollingJob.Reschedule(ctx); err != nil {
72+
if err := newScheduler.RescheduleJob(ctx, imagePollingJob); err != nil {
8873
slog.WarnContext(ctx, "Failed to reschedule image-polling job", "error", err)
8974
}
90-
if err := autoUpdateJob.Reschedule(ctx); err != nil {
75+
if err := newScheduler.RescheduleJob(ctx, autoUpdateJob); err != nil {
9176
slog.WarnContext(ctx, "Failed to reschedule auto-update job", "error", err)
9277
}
9378
if !appConfig.AgentMode {
94-
if err := environmentHealthJob.Reschedule(ctx); err != nil {
79+
if err := newScheduler.RescheduleJob(ctx, environmentHealthJob); err != nil {
9580
slog.WarnContext(ctx, "Failed to reschedule environment-health job", "error", err)
9681
}
9782
}
9883
}
9984
appServices.Settings.OnAutoUpdateSettingsChanged = func(ctx context.Context) {
10085
slog.DebugContext(ctx, "AutoUpdateSettingsChanged callback triggered")
101-
if err := autoUpdateJob.Reschedule(ctx); err != nil {
86+
if err := newScheduler.RescheduleJob(ctx, autoUpdateJob); err != nil {
10287
slog.WarnContext(ctx, "Failed to reschedule auto-update job", "error", err)
10388
}
10489
}
@@ -110,7 +95,7 @@ func setupSettingsCallbacks(appServices *Services, appConfig *config.Config, ima
11095
}
11196
}
11297
appServices.Settings.OnScheduledPruneSettingsChanged = func(ctx context.Context) {
113-
if err := scheduledPruneJob.Reschedule(ctx); err != nil {
98+
if err := newScheduler.RescheduleJob(ctx, scheduledPruneJob); err != nil {
11499
slog.WarnContext(ctx, "Failed to reschedule scheduled-prune job", "error", err)
115100
}
116101
}

0 commit comments

Comments
 (0)