Skip to content

Commit 616a3cd

Browse files
authored
Merge pull request #1723 from diggerhq/feat-drift-app
feat drift app
2 parents 0720b5b + 2465514 commit 616a3cd

35 files changed

+5485
-1
lines changed

.github/workflows/drift_deploy.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: driftapp Deploy
2+
on:
3+
push:
4+
branches:
5+
- develop # change to main if needed
6+
- feat-drift-app
7+
jobs:
8+
deploy:
9+
name: Deploy app
10+
runs-on: ubuntu-latest
11+
concurrency: deploy-group # optional: ensure only one action runs at a time
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: superfly/flyctl-actions/setup-flyctl@master
15+
- run: flyctl deploy --remote-only --config fly-drift.toml
16+
env:
17+
FLY_API_TOKEN: ${{ secrets.FLYIO_DRIFT_TOKEN }}

Dockerfile_drift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
FROM golang:1.22 as builder
2+
ARG COMMIT_SHA
3+
RUN echo "commit sha: ${COMMIT_SHA}"
4+
5+
# Set the working directory
6+
WORKDIR $GOPATH/src/github.com/diggerhq/digger
7+
8+
# Copy all required source, blacklist files that are not required through `.dockerignore`
9+
COPY . .
10+
11+
# Get the vendor library
12+
RUN go version
13+
14+
# RUN vgo install
15+
16+
# https://github.com/ethereum/go-ethereum/issues/2738
17+
# Build static binary "-getmode=vendor" does not work with go-ethereum
18+
19+
RUN go build -ldflags="-X 'main.Version=${COMMIT_SHA}'" -o drift_exe ./ee/drift/
20+
21+
# Multi-stage build will just copy the binary to an alpine image.
22+
FROM ubuntu:24.04 as runner
23+
ENV ATLAS_VERSION v0.16.0
24+
ARG COMMIT_SHA
25+
WORKDIR /app
26+
27+
RUN apt-get update && apt-get install -y ca-certificates curl && apt-get install -y git && apt-get clean all
28+
RUN update-ca-certificates
29+
30+
RUN echo "commit sha: ${COMMIT_SHA}"
31+
32+
# install atlas
33+
RUN curl -sSf https://atlasgo.sh | sh
34+
35+
36+
37+
# Set gin to production
38+
#ENV GIN_MODE=release
39+
40+
# Expose the running port
41+
EXPOSE 3000
42+
43+
# Copy the binary to the corresponding folder
44+
COPY --from=builder /go/src/github.com/diggerhq/digger/drift_exe /app/driftapp
45+
COPY --from=builder /go/src/github.com/diggerhq/digger/ee/drift/scripts/entrypoint.sh /app/entrypoint.sh
46+
ADD ee/backend/templates ./templates
47+
48+
# Run the binary
49+
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]

ee/drift/.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# flyctl launch added from .gitignore
2+
**/.env
3+
fly.toml

ee/drift/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.env
2+
main
3+
drift

ee/drift/Dockerfile

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
FROM golang:1.22 as builder
2+
ARG COMMIT_SHA
3+
RUN echo "commit sha: ${COMMIT_SHA}"
4+
5+
# Set the working directory
6+
WORKDIR $GOPATH/src/github.com/diggerhq/drift
7+
8+
# Copy all required source, blacklist files that are not required through `.dockerignore`
9+
COPY . .
10+
11+
# Get the vendor library
12+
RUN go version
13+
14+
# RUN vgo install
15+
16+
# https://github.com/ethereum/go-ethereum/issues/2738
17+
# Build static binary "-getmode=vendor" does not work with go-ethereum
18+
19+
RUN go build -ldflags="-X 'main.Version=${COMMIT_SHA}'" -o driftapp_exe ./
20+
21+
# Multi-stage build will just copy the binary to an alpine image.
22+
FROM ubuntu:24.04 as runner
23+
ENV ATLAS_VERSION v0.16.0
24+
ARG COMMIT_SHA
25+
WORKDIR /app
26+
27+
RUN apt-get update && apt-get install -y ca-certificates curl && apt-get install -y git && apt-get clean all
28+
RUN update-ca-certificates
29+
30+
RUN echo "commit sha: ${COMMIT_SHA}"
31+
32+
33+
# Set gin to production
34+
#ENV GIN_MODE=release
35+
36+
# Expose the running port
37+
EXPOSE 3000
38+
39+
# Copy the binary to the corresponding folder
40+
COPY --from=builder /go/src/github.com/diggerhq/drift/driftapp_exe /app/driftapp
41+
COPY --from=builder /go/src/github.com/diggerhq/drift/scripts/entrypoint.sh /app/entrypoint.sh
42+
43+
# Run the binary
44+
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]

ee/drift/controllers/controllers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package controllers
2+
3+
import "github.com/diggerhq/digger/next/utils"
4+
5+
type MainController struct {
6+
GithubClientProvider utils.GithubClientProvider
7+
}

ee/drift/controllers/github.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/diggerhq/digger/ee/drift/dbmodels"
8+
"github.com/diggerhq/digger/ee/drift/middleware"
9+
"github.com/diggerhq/digger/ee/drift/model"
10+
next_utils "github.com/diggerhq/digger/next/utils"
11+
"github.com/gin-gonic/gin"
12+
"github.com/google/go-github/v61/github"
13+
"golang.org/x/oauth2"
14+
"log"
15+
"net/http"
16+
"os"
17+
"strconv"
18+
"strings"
19+
)
20+
21+
func (mc MainController) GithubAppCallbackPage(c *gin.Context) {
22+
installationId := c.Request.URL.Query()["installation_id"][0]
23+
//setupAction := c.Request.URL.Query()["setup_action"][0]
24+
code := c.Request.URL.Query()["code"][0]
25+
clientId := os.Getenv("GITHUB_APP_CLIENT_ID")
26+
clientSecret := os.Getenv("GITHUB_APP_CLIENT_SECRET")
27+
28+
installationId64, err := strconv.ParseInt(installationId, 10, 64)
29+
if err != nil {
30+
log.Printf("err: %v", err)
31+
c.String(http.StatusInternalServerError, "Failed to parse installation_id.")
32+
return
33+
}
34+
35+
result, installation, err := validateGithubCallback(mc.GithubClientProvider, clientId, clientSecret, code, installationId64)
36+
if !result {
37+
log.Printf("Failed to validated installation id, %v\n", err)
38+
c.String(http.StatusInternalServerError, "Failed to validate installation_id.")
39+
return
40+
}
41+
42+
// retrive org for current orgID
43+
orgId, exists := c.Get(middleware.ORGANISATION_ID_KEY)
44+
if !exists {
45+
log.Printf("missing argument orgId in github callback")
46+
c.String(http.StatusBadRequest, "missing orgID in request")
47+
return
48+
}
49+
org, err := dbmodels.DB.GetOrganisationById(orgId)
50+
if err != nil {
51+
log.Printf("Error fetching organisation: %v", err)
52+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error fetching organisation"})
53+
return
54+
}
55+
56+
// create a github installation link (org ID matched to installation ID)
57+
_, err = dbmodels.DB.CreateGithubInstallationLink(org.ID, installationId)
58+
if err != nil {
59+
log.Printf("Error saving GithubInstallationLink to database: %v", err)
60+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error updating GitHub installation"})
61+
return
62+
}
63+
64+
client, _, err := mc.GithubClientProvider.Get(*installation.AppID, installationId64)
65+
if err != nil {
66+
log.Printf("Error retriving github client: %v", err)
67+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error fetching organisation"})
68+
return
69+
70+
}
71+
72+
// we get repos accessible to this installation
73+
listRepos, _, err := client.Apps.ListRepos(context.Background(), nil)
74+
if err != nil {
75+
log.Printf("Failed to validated list existing repos, %v\n", err)
76+
c.String(http.StatusInternalServerError, "Failed to list existing repos: %v", err)
77+
return
78+
}
79+
repos := listRepos.Repositories
80+
81+
// reset all existing repos (soft delete)
82+
var ExistingRepos []model.Repo
83+
err = dbmodels.DB.GormDB.Delete(ExistingRepos, "organization_id=?", orgId).Error
84+
if err != nil {
85+
log.Printf("could not delete repos: %v", err)
86+
c.String(http.StatusInternalServerError, "could not delete repos: %v", err)
87+
return
88+
}
89+
90+
// here we mark repos that are available one by one
91+
for _, repo := range repos {
92+
repoFullName := *repo.FullName
93+
repoOwner := strings.Split(*repo.FullName, "/")[0]
94+
repoName := *repo.Name
95+
repoUrl := fmt.Sprintf("https://github.com/%v", repoFullName)
96+
97+
_, _, err = dbmodels.CreateOrGetDiggerRepoForGithubRepo(repoFullName, repoOwner, repoName, repoUrl, installationId64, *installation.AppID, *installation.Account.ID, *installation.Account.Login)
98+
if err != nil {
99+
log.Printf("createOrGetDiggerRepoForGithubRepo error: %v", err)
100+
c.String(http.StatusInternalServerError, "createOrGetDiggerRepoForGithubRepo error: %v", err)
101+
return
102+
}
103+
}
104+
105+
c.HTML(http.StatusOK, "github_success.tmpl", gin.H{})
106+
}
107+
108+
// why this validation is needed: https://roadie.io/blog/avoid-leaking-github-org-data/
109+
// validation based on https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app , step 3
110+
func validateGithubCallback(githubClientProvider next_utils.GithubClientProvider, clientId string, clientSecret string, code string, installationId int64) (bool, *github.Installation, error) {
111+
ctx := context.Background()
112+
type OAuthAccessResponse struct {
113+
AccessToken string `json:"access_token"`
114+
}
115+
httpClient := http.Client{}
116+
117+
githubHostname := "github.com"
118+
reqURL := fmt.Sprintf("https://%v/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", githubHostname, clientId, clientSecret, code)
119+
req, err := http.NewRequest(http.MethodPost, reqURL, nil)
120+
if err != nil {
121+
return false, nil, fmt.Errorf("could not create HTTP request: %v\n", err)
122+
}
123+
req.Header.Set("accept", "application/json")
124+
125+
res, err := httpClient.Do(req)
126+
if err != nil {
127+
return false, nil, fmt.Errorf("request to login/oauth/access_token failed: %v\n", err)
128+
}
129+
130+
if err != nil {
131+
return false, nil, fmt.Errorf("Failed to read response's body: %v\n", err)
132+
}
133+
134+
var t OAuthAccessResponse
135+
if err := json.NewDecoder(res.Body).Decode(&t); err != nil {
136+
return false, nil, fmt.Errorf("could not parse JSON response: %v\n", err)
137+
}
138+
139+
ts := oauth2.StaticTokenSource(
140+
&oauth2.Token{AccessToken: t.AccessToken},
141+
)
142+
tc := oauth2.NewClient(ctx, ts)
143+
//tc := &http.Client{
144+
// Transport: &oauth2.Transport{
145+
// Base: httpClient.Transport,
146+
// Source: oauth2.ReuseTokenSource(nil, ts),
147+
// },
148+
//}
149+
150+
client, err := githubClientProvider.NewClient(tc)
151+
if err != nil {
152+
log.Printf("could create github client: %v", err)
153+
return false, nil, fmt.Errorf("could not create github client: %v", err)
154+
}
155+
156+
installationIdMatch := false
157+
// list all installations for the user
158+
var matchedInstallation *github.Installation
159+
installations, _, err := client.Apps.ListUserInstallations(ctx, nil)
160+
if err != nil {
161+
log.Printf("could not retrieve installations: %v", err)
162+
return false, nil, fmt.Errorf("could not retrieve installations: %v", installationId)
163+
}
164+
log.Printf("installations %v", installations)
165+
for _, v := range installations {
166+
log.Printf("installation id: %v\n", *v.ID)
167+
if *v.ID == installationId {
168+
matchedInstallation = v
169+
installationIdMatch = true
170+
}
171+
}
172+
if !installationIdMatch {
173+
return false, nil, fmt.Errorf("InstallationId %v doesn't match any id for specified user\n", installationId)
174+
}
175+
176+
return true, matchedInstallation, nil
177+
}

ee/drift/controllers/health.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package controllers
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
)
6+
7+
func (mc MainController) Ping(c *gin.Context) {
8+
c.String(200, "pong")
9+
}

ee/drift/dbgen/dbgen.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"gorm.io/driver/postgres"
7+
"gorm.io/gen"
8+
"gorm.io/gorm"
9+
)
10+
11+
// Dynamic SQL
12+
type Querier interface {
13+
// SELECT * FROM @@table WHERE name = @name{{if role !=""}} AND role = @role{{end}}
14+
FilterWithNameAndRole(name, role string) ([]gen.T, error)
15+
}
16+
17+
func main() {
18+
g := gen.NewGenerator(gen.Config{
19+
OutPath: "../models_generated",
20+
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
21+
})
22+
23+
dburl := os.Getenv("DB_URL")
24+
if dburl == "" {
25+
dburl = "postgresql://postgres:[email protected]:54322/postgres"
26+
}
27+
gormdb, _ := gorm.Open(postgres.Open(dburl))
28+
g.UseDB(gormdb) // reuse your gorm db
29+
30+
g.ApplyBasic(
31+
// Generate structs from all tables of current database
32+
g.GenerateAllTable()...,
33+
)
34+
35+
// Generate the code
36+
g.Execute()
37+
}

ee/drift/dbgen/go.mod

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module dbgen
2+
3+
go 1.22.0
4+
5+
require (
6+
gorm.io/driver/postgres v1.5.9
7+
gorm.io/gen v0.3.26
8+
gorm.io/gorm v1.25.12
9+
)
10+
11+
require (
12+
github.com/go-sql-driver/mysql v1.7.0 // indirect
13+
github.com/jackc/pgpassfile v1.0.0 // indirect
14+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
15+
github.com/jackc/pgx/v5 v5.5.5 // indirect
16+
github.com/jackc/puddle/v2 v2.2.1 // indirect
17+
github.com/jinzhu/inflection v1.0.0 // indirect
18+
github.com/jinzhu/now v1.1.5 // indirect
19+
golang.org/x/crypto v0.17.0 // indirect
20+
golang.org/x/mod v0.17.0 // indirect
21+
golang.org/x/sync v0.8.0 // indirect
22+
golang.org/x/text v0.18.0 // indirect
23+
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
24+
gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c // indirect
25+
gorm.io/driver/mysql v1.5.7 // indirect
26+
gorm.io/hints v1.1.0 // indirect
27+
gorm.io/plugin/dbresolver v1.5.3 // indirect
28+
)

0 commit comments

Comments
 (0)