Skip to content

Commit 752c626

Browse files
authored
feat: enable orchestrator gha setup via UI, feat: full docker compose, fix: handle drift job status processing in orchestrator (#2569)
* feat: docker compose for full hosting set up * feat: use postgres for statesman in docker compose * fix: sidecar health check endpoint * chore: move E2B API key to .env file * fix: update DIGGER_DRIFT_REPORTER_HOSTNAME * Add all necessary env vars to docker compose file * Fix: duplicatre drift state machine logic to orchestrator * handle legacy webhook route * add postgres for token service, add compose profiles
1 parent 54070bc commit 752c626

File tree

23 files changed

+725
-73
lines changed

23 files changed

+725
-73
lines changed

Dockerfile_backend

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
2424
--mount=type=cache,target=/go/pkg/mod \
2525
go build -ldflags="-X 'main.Version=${COMMIT_SHA}'" -o backend_exe ./backend/
2626

27+
# Build the projects refresh service binary and ship it in the image.
28+
# This is required for "project cache local exec" mode, which expects to exec
29+
# the refresh service locally within the container.
30+
RUN --mount=type=cache,target=/root/.cache/go-build \
31+
--mount=type=cache,target=/go/pkg/mod \
32+
go build -ldflags="-X 'main.Version=${COMMIT_SHA}'" -o /tmp/projects_refesh_main ./background/projects-refresh-service
33+
2734
# Multi-stage build will just copy the binary to an alpine image.
2835
FROM ubuntu:24.04 as runner
2936
ENV ATLAS_VERSION v0.38.0
@@ -48,6 +55,7 @@ EXPOSE 3000
4855

4956
# Copy the binary to the corresponding folder
5057
COPY --from=builder /go/src/github.com/diggerhq/digger/backend_exe /app/backend
58+
COPY --from=builder /tmp/projects_refesh_main /app/projects_refesh_main
5159
COPY --from=builder /go/src/github.com/diggerhq/digger/backend/scripts/entrypoint.sh /app/entrypoint.sh
5260
COPY --from=builder /go/src/github.com/diggerhq/digger/backend/migrations /app/migrations
5361
ADD backend/templates ./templates

backend/bootstrap/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ func Bootstrap(templates embed.FS, diggerController controllers.DiggerController
159159
r.LoadHTMLGlob("templates/*.tmpl")
160160
}
161161

162+
// Canonical GitHub App webhook endpoint.
163+
r.POST("/github/webhook", diggerController.GithubAppWebHook)
164+
// Legacy webhook path kept for backward compatibility.
162165
r.POST("/github-app-webhook", diggerController.GithubAppWebHook)
163166

164167
tenantActionsGroup := r.Group("/api/tenants")
@@ -173,6 +176,20 @@ func Bootstrap(templates embed.FS, diggerController controllers.DiggerController
173176
githubGroup.GET("/setup", controllers.GithubAppSetup)
174177
githubGroup.GET("/exchange-code", diggerController.GithubSetupExchangeCode)
175178

179+
publicPrefix := utils.NormalizePublicPathPrefix(os.Getenv("DIGGER_PUBLIC_PATH_PREFIX"))
180+
if publicPrefix != "" {
181+
prefixed := r.Group(publicPrefix)
182+
prefixed.POST("/github/webhook", diggerController.GithubAppWebHook)
183+
prefixed.POST("/github-app-webhook", diggerController.GithubAppWebHook)
184+
185+
prefixedGithubGroup := prefixed.Group("/github")
186+
prefixedGithubGroup.Use(middleware.GetWebMiddleware())
187+
prefixed.GET("/github/callback", diggerController.GithubAppCallbackPage)
188+
prefixedGithubGroup.GET("/repos", diggerController.GithubReposPage)
189+
prefixedGithubGroup.GET("/setup", controllers.GithubAppSetup)
190+
prefixedGithubGroup.GET("/exchange-code", diggerController.GithubSetupExchangeCode)
191+
}
192+
176193
authorized := r.Group("/")
177194
authorized.Use(middleware.GetApiMiddleware(), middleware.AccessLevel(models.CliJobAccessType, models.AccessPolicyType, models.AdminPolicyType))
178195

backend/controllers/github_setup.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,37 @@ func GithubAppSetup(c *gin.Context) {
3737
}
3838

3939
host := os.Getenv("HOSTNAME")
40+
// When the backend is deployed behind a reverse proxy (or behind the UI proxy),
41+
// the inbound request Host/TLS reflects the internal hop, not the public origin.
42+
// The GitHub App manifest flow requires public callback/webhook URLs, so we
43+
// prefer X-Forwarded-Host/Proto when present to construct externally reachable
44+
// URLs.
45+
forwardedHost := c.Request.Header.Get("X-Forwarded-Host")
46+
forwardedProto := c.Request.Header.Get("X-Forwarded-Proto")
47+
if forwardedHost != "" {
48+
if forwardedProto == "" {
49+
forwardedProto = "https"
50+
}
51+
host = fmt.Sprintf("%s://%s", forwardedProto, forwardedHost)
52+
} else if host == "" {
53+
scheme := "http"
54+
if c.Request.TLS != nil {
55+
scheme = "https"
56+
}
57+
host = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
58+
}
59+
publicPrefix := utils.NormalizePublicPathPrefix(os.Getenv("DIGGER_PUBLIC_PATH_PREFIX"))
4060
manifest := &githubAppRequest{
4161
Name: fmt.Sprintf("Digger app %v", rand.Int31()),
4262
Description: fmt.Sprintf("Digger hosted at %s", host),
4363
URL: host,
44-
RedirectURL: fmt.Sprintf("%s/github/exchange-code", host),
64+
RedirectURL: fmt.Sprintf("%s%s", host, utils.ApplyPublicPathPrefix(publicPrefix, "/github/exchange-code")),
4565
Public: false,
4666
Webhook: &githubWebhook{
4767
Active: true,
48-
URL: fmt.Sprintf("%s/github-app-webhook", host),
68+
URL: fmt.Sprintf("%s%s", host, utils.ApplyPublicPathPrefix(publicPrefix, "/github/webhook")),
4969
},
50-
CallbackUrls: []string{fmt.Sprintf("%s/github/callback", host)},
70+
CallbackUrls: []string{fmt.Sprintf("%s%s", host, utils.ApplyPublicPathPrefix(publicPrefix, "/github/callback"))},
5171
SetupOnUpdate: true,
5272
RequestOauthOnInstall: true,
5373
Events: []string{

backend/controllers/projects.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,40 @@ func (d DiggerController) SetJobStatusForProject(c *gin.Context) {
927927

928928
// store digger job summary
929929
if request.JobSummary != nil {
930-
models.DB.UpdateDiggerJobSummary(job.DiggerJobID, request.JobSummary.ResourcesCreated, request.JobSummary.ResourcesUpdated, request.JobSummary.ResourcesDeleted)
930+
job, err = models.DB.UpdateDiggerJobSummary(job.DiggerJobID, request.JobSummary.ResourcesCreated, request.JobSummary.ResourcesUpdated, request.JobSummary.ResourcesDeleted)
931+
if err != nil {
932+
slog.Warn("Could not update digger job summary for drift handling",
933+
"jobId", jobId,
934+
"error", err,
935+
)
936+
}
937+
938+
isDriftJob, err := IsDriftStatusJob(job)
939+
if err != nil {
940+
slog.Warn("Could not determine if job is a drift job",
941+
"jobId", jobId,
942+
"error", err,
943+
)
944+
} else if isDriftJob {
945+
project, err := models.DB.GetProjectByName(orgId, job.Batch.RepoFullName, job.ProjectName)
946+
if err != nil {
947+
slog.Warn("Could not load project for drift state update",
948+
"jobId", jobId,
949+
"projectName", job.ProjectName,
950+
"repoFullName", job.Batch.RepoFullName,
951+
"error", err,
952+
)
953+
} else {
954+
err = ProjectDriftStateMachineApply(*project, job.TerraformOutput, request.JobSummary.ResourcesCreated, request.JobSummary.ResourcesUpdated, request.JobSummary.ResourcesDeleted)
955+
if err != nil {
956+
slog.Warn("Could not update project drift state",
957+
"jobId", jobId,
958+
"projectName", job.ProjectName,
959+
"error", err,
960+
)
961+
}
962+
}
963+
}
931964
}
932965

933966
// Update PR comment with real-time status for succeeded job

backend/controllers/projects_helpers.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,66 @@ import (
55
"fmt"
66
"log/slog"
77
"os"
8+
"strings"
9+
"time"
810
"unicode/utf8"
911

1012
"github.com/diggerhq/digger/backend/models"
1113
"github.com/diggerhq/digger/backend/utils"
1214
"github.com/diggerhq/digger/libs/ci/github"
1315
"github.com/diggerhq/digger/libs/digger_config"
1416
orchestrator_scheduler "github.com/diggerhq/digger/libs/scheduler"
17+
"github.com/diggerhq/digger/libs/spec"
1518
)
1619

20+
func IsDriftStatusJob(job *models.DiggerJob) (bool, error) {
21+
if job == nil || job.Batch == nil {
22+
return false, nil
23+
}
24+
25+
if job.Batch.BatchType != orchestrator_scheduler.DiggerCommandPlan || job.Batch.PrNumber != 0 {
26+
return false, nil
27+
}
28+
29+
var vcsSpec spec.VcsSpec
30+
err := json.Unmarshal(job.SerializedVcsSpec, &vcsSpec)
31+
if err != nil {
32+
return false, err
33+
}
34+
35+
return strings.EqualFold(vcsSpec.VcsType, "noop"), nil
36+
}
37+
38+
func ProjectDriftStateMachineApply(project models.Project, tfplan string, resourcesCreated uint, resourcesUpdated uint, resourcesDeleted uint) error {
39+
isEmptyPlan := resourcesCreated == 0 && resourcesUpdated == 0 && resourcesDeleted == 0
40+
wasEmptyPlan := project.DriftToCreate == 0 && project.DriftToUpdate == 0 && project.DriftToDelete == 0
41+
if isEmptyPlan {
42+
project.DriftStatus = models.DriftStatusNoDrift
43+
}
44+
if !isEmptyPlan && wasEmptyPlan {
45+
project.DriftStatus = models.DriftStatusNewDrift
46+
}
47+
if !isEmptyPlan && !wasEmptyPlan {
48+
if project.DriftTerraformPlan != tfplan {
49+
if project.DriftStatus == models.DriftStatusAcknowledgeDrift {
50+
project.DriftStatus = models.DriftStatusNewDrift
51+
}
52+
}
53+
}
54+
55+
project.DriftTerraformPlan = tfplan
56+
project.DriftToCreate = resourcesCreated
57+
project.DriftToUpdate = resourcesUpdated
58+
project.DriftToDelete = resourcesDeleted
59+
project.LatestDriftCheck = time.Now()
60+
result := models.DB.GormDB.Save(&project)
61+
if result.Error != nil {
62+
return result.Error
63+
}
64+
65+
return nil
66+
}
67+
1768
func GenerateChecksSummaryForBatch(batch *models.DiggerBatch) (string, error) {
1869
summaryEndpoint := os.Getenv("DIGGER_AI_SUMMARY_ENDPOINT")
1970
if summaryEndpoint == "" {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package utils
2+
3+
import "strings"
4+
5+
// NormalizePublicPathPrefix normalizes a configured URL path prefix (e.g. "/orchestrator")
6+
// so it can be safely concatenated with other paths.
7+
//
8+
// Examples:
9+
// - "" or "/" -> ""
10+
// - "orchestrator" -> "/orchestrator"
11+
// - "/orchestrator/" -> "/orchestrator"
12+
func NormalizePublicPathPrefix(raw string) string {
13+
raw = strings.TrimSpace(raw)
14+
if raw == "" || raw == "/" {
15+
return ""
16+
}
17+
if !strings.HasPrefix(raw, "/") {
18+
raw = "/" + raw
19+
}
20+
return strings.TrimRight(raw, "/")
21+
}
22+
23+
// ApplyPublicPathPrefix concatenates prefix + p ensuring p starts with "/".
24+
func ApplyPublicPathPrefix(prefix, p string) string {
25+
if p == "" {
26+
return prefix
27+
}
28+
if !strings.HasPrefix(p, "/") {
29+
p = "/" + p
30+
}
31+
return prefix + p
32+
}

0 commit comments

Comments
 (0)