login with oauth2 & update commit status

This commit is contained in:
Lunny Xiao 2021-10-27 15:26:00 +08:00
parent 1cd16acc2b
commit 2422b739f5
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
14 changed files with 1186 additions and 43 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
pr-deployer
pr-deployer
config.yaml

18
config.example.yaml Normal file
View File

@ -0,0 +1,18 @@
ServiceType: github
RepoURL: https://github.com/go-gitea/gitea
OAuth2:
ServerURL: https://github.com
ClientID: xxxxxx
ClientSecret: xxxxxxxx
CallbackURL: http://localhost:3001
Domain: localhost
DomainIP: localhost
WebhookSecretKey: ''
CodeCacheDir: './data'
CloudflareToken: ''
CloudflareEmail: ''
Proxy:
Enabled: false
ProxyURL: 'socks5:127.0.0.1:1080'
ProxyHosts: github.com

35
go.mod
View File

@ -3,37 +3,68 @@ module gitea.com/gitea/pr-deployer
go 1.17
require (
gitea.com/go-chi/session v0.0.0-20211013065435-7d334f340c09
github.com/cloudflare/cloudflare-go v0.26.0
github.com/docker/docker v20.10.10+incompatible
github.com/go-chi/chi/v5 v5.0.4
github.com/go-git/go-git/v5 v5.4.2
github.com/gobwas/glob v0.2.3
github.com/google/go-github/v39 v39.2.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.8.1
github.com/unrolled/render v1.4.0
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
)
require (
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
google.golang.org/grpc v1.38.0 // indirect
google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

622
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,18 @@
package main
import (
"fmt"
"log"
"gitea.com/gitea/pr-deployer/cmd"
"gitea.com/gitea/pr-deployer/pkgs/settings"
)
func main() {
if err := settings.Init(); err != nil {
log.Fatal(err)
}
if err := cmd.Execute(); err != nil {
fmt.Println(err)
log.Fatal(err)
}
}

35
pkgs/github/github.go Normal file
View File

@ -0,0 +1,35 @@
package github
import (
"crypto/tls"
"net/http"
"net/url"
"gitea.com/gitea/pr-deployer/pkgs/proxy"
"gitea.com/gitea/pr-deployer/pkgs/settings"
"github.com/google/go-github/v39/github"
"golang.org/x/oauth2"
)
func GetGithubClient(token *oauth2.Token) *github.Client {
ts := oauth2.StaticTokenSource(token)
var client = &http.Client{
Transport: &oauth2.Transport{
Base: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(req *http.Request) (*url.URL, error) {
return proxy.Proxy()(req)
},
},
Source: oauth2.ReuseTokenSource(nil, ts),
},
}
githubClient := github.NewClient(client)
if settings.BaseURL != "https://github.com" {
githubClient, _ = github.NewEnterpriseClient(settings.BaseURL, settings.BaseURL, client)
}
return githubClient
}

85
pkgs/proxy/proxy.go Normal file
View File

@ -0,0 +1,85 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package proxy
import (
"net/http"
"net/url"
"os"
"sync"
"gitea.com/gitea/pr-deployer/pkgs/settings"
"github.com/gobwas/glob"
log "github.com/sirupsen/logrus"
)
var (
once sync.Once
hostMatchers []glob.Glob
)
// GetProxyURL returns proxy url
func GetProxyURL() string {
if !settings.Proxy.Enabled {
return ""
}
if settings.Proxy.ProxyURL == "" {
if os.Getenv("http_proxy") != "" {
return os.Getenv("http_proxy")
}
return os.Getenv("https_proxy")
}
return settings.Proxy.ProxyURL
}
// Match return true if url needs to be proxied
func Match(u string) bool {
if !settings.Proxy.Enabled {
return false
}
// enforce do once
Proxy()
for _, v := range hostMatchers {
if v.Match(u) {
return true
}
}
return false
}
// Proxy returns the system proxy
func Proxy() func(req *http.Request) (*url.URL, error) {
if !settings.Proxy.Enabled {
return func(req *http.Request) (*url.URL, error) {
return nil, nil
}
}
if settings.Proxy.ProxyURL == "" {
return http.ProxyFromEnvironment
}
once.Do(func() {
for _, h := range settings.Proxy.ProxyHosts {
if g, err := glob.Compile(h); err == nil {
hostMatchers = append(hostMatchers, g)
} else {
log.Error("glob.Compile %s failed: %v", h, err)
}
}
})
return func(req *http.Request) (*url.URL, error) {
for _, v := range hostMatchers {
if v.Match(req.URL.Host) {
return http.ProxyURL(settings.Proxy.ProxyURLFixed)(req)
}
}
return http.ProxyFromEnvironment(req)
}
}

25
pkgs/services/pulls.go Normal file
View File

@ -0,0 +1,25 @@
package services
import (
"context"
my_github "gitea.com/gitea/pr-deployer/pkgs/github"
"gitea.com/gitea/pr-deployer/pkgs/settings"
"github.com/google/go-github/v39/github"
"golang.org/x/oauth2"
)
func GetPullRequests(token *oauth2.Token, p int) ([]*github.PullRequest, error) {
c := my_github.GetGithubClient(token)
pulls, _, err := c.PullRequests.List(context.Background(), settings.RepoOwner, settings.RepoName, &github.PullRequestListOptions{
Sort: "updated",
Direction: "desc",
ListOptions: github.ListOptions{
Page: p,
PerPage: 50,
},
})
return pulls, err
}

View File

@ -3,6 +3,7 @@ package services
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
@ -10,13 +11,94 @@ import (
"gitea.com/gitea/pr-deployer/pkgs/settings"
"github.com/cloudflare/cloudflare-go"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/google/go-github/v39/github"
)
func UpdateAndStartPullRequest(number int) error {
func UpdateAndStartPullRequest(ctx context.Context, number int, sha string) error {
// 0 send commit status
if err := updateCommitStatus(ctx, number, sha, statusPending, "", ""); err != nil {
return err
}
// 1 download the git
p := filepath.Join(settings.CodeCacheDir, strconv.Itoa(number))
if err := updateGitRepo(number); err != nil {
return err
}
// 2 change domain
if err := checkAndUpdateSubDomain(number); err != nil {
return err
}
// 3 build
if err := buildImage(ctx, number); err != nil {
return err
}
// 4 change reverse server
if err := runImage(ctx, number); err != nil {
return err
}
// 5 send commit status
if err := updateCommitStatus(ctx, number, sha, statusSuccess, "", fmt.Sprintf("https://try-pr-%d.gitea.io", number)); err != nil {
return err
}
return nil
}
func runImage(ctx context.Context, number int) error {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return err
}
_, err = cli.ContainerExecCreate(ctx, "gitea/gitea:"+getImageTag(number), types.ExecConfig{
WorkingDir: getPRDir(number),
})
return err
}
func getRepoName() string {
return "gitea"
}
func getOwnerName() string {
return "go-gitea"
}
const (
statusPending = "pending"
statusSuccess = "success"
statusError = "error"
statusFailure = "failure"
)
// updateCommitStatus
func updateCommitStatus(ctx context.Context, number int, sha, status, desc, targetURL string) error {
c := github.NewClient(nil)
// pending, success, error, or failure
_, _, err := c.Repositories.CreateStatus(ctx, getOwnerName(), getRepoName(), sha, &github.RepoStatus{
State: github.String(status),
TargetURL: github.String(targetURL),
Description: github.String(desc),
Context: github.String("pr-deployer"),
// AvatarURL:
})
return err
}
func getPRDir(number int) string {
return filepath.Join(settings.CodeCacheDir, strconv.Itoa(number))
}
func updateGitRepo(number int) error {
p := getPRDir(number)
fmt.Println("clone code into", p)
// 1.1 git fetch origin +refs/heads/main
@ -47,17 +129,26 @@ func UpdateAndStartPullRequest(number int) error {
if err != nil {
return err
}
return nil
}
// 2 build
func getImageTag(number int) string {
return fmt.Sprintf("pr-%d", number)
}
// 3 change domain
if err := checkAndUpdateSubDomain(number); err != nil {
// buildImage build the docker image via Dockerfile
func buildImage(ctx context.Context, number int) error {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return err
}
// 4 change reverse server
// 5 send commit status
var buildContext io.Reader
if _, err = cli.ImageBuild(ctx, buildContext, types.ImageBuildOptions{
Tags: []string{getImageTag(number)},
}); err != nil {
return err
}
return nil
}

View File

@ -1,10 +1,126 @@
package settings
var (
Domain string
DomainIP string
WebhookSecretKey []byte
CodeCacheDir string
CloudflareToken string
CloudflareEmail string
import (
"errors"
"fmt"
"net/url"
"os"
"strings"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)
var (
ServiceType string
RepoURL string
BaseURL string
RepoOwner string
RepoName string
OAuth2CallbackURL string
OAuth2ServerURL string
OAuth2ClientID string
OAuth2ClientSecret string
Domain string
DomainIP string
WebhookSecretKey []byte
CodeCacheDir string
CloudflareToken string
CloudflareEmail string
Proxy struct {
Enabled bool
ProxyURL string
ProxyURLFixed *url.URL
ProxyHosts []string
}
)
var (
OAuth2Conf *oauth2.Config
)
func Init() error {
viper.SetConfigFile("./config.yaml")
if err := viper.ReadInConfig(); err != nil {
return err
}
ServiceType = viper.GetString("ServiceType")
if ServiceType == "" {
return errors.New("ServiceType cannot be empty")
}
if !strings.EqualFold(ServiceType, "github") && !strings.EqualFold(ServiceType, "gitea") {
return fmt.Errorf("unknow service type: %s", ServiceType)
}
RepoURL = viper.GetString("RepoURL")
if RepoURL == "" {
return errors.New("RepoURL cannot be empty")
}
u, err := url.Parse(RepoURL)
if err != nil {
return err
}
BaseURL = u.Scheme + "://" + u.Host
var p = u.Path
if strings.HasPrefix(p, "/") {
p = p[1:]
}
fields := strings.Split(p, "/")
RepoOwner = fields[0]
RepoName = fields[1]
OAuth2CallbackURL = viper.GetString("OAuth2.CallbackURL")
if OAuth2CallbackURL == "" {
return errors.New("OAuth2CallbackURL cannot be empty")
}
OAuth2ServerURL = viper.GetString("OAuth2.ServerURL")
if OAuth2ServerURL == "" {
return errors.New("OAuth2ServerURL cannot be empty")
}
OAuth2ClientID = viper.GetString("OAuth2.ClientID")
if OAuth2ClientID == "" {
return errors.New("OAuth2ClientID cannot be empty")
}
OAuth2ClientSecret = viper.GetString("OAuth2.ClientSecret")
if OAuth2ClientSecret == "" {
return errors.New("OAuth2ClientSecret cannot be empty")
}
Domain = viper.GetString("Domain")
DomainIP = viper.GetString("DomainIP")
WebhookSecretKey = []byte(viper.GetString("WebhookSecretKey"))
CodeCacheDir = viper.GetString("CodeCacheDir")
if err := os.MkdirAll(CodeCacheDir, os.ModePerm); err != nil {
return err
}
CloudflareToken = viper.GetString("CloudflareToken")
CloudflareEmail = viper.GetString("CloudflareEmail")
OAuth2Conf = &oauth2.Config{
ClientID: OAuth2ClientID,
ClientSecret: OAuth2ClientSecret, // change this to your gitea secret id
Endpoint: oauth2.Endpoint{
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", OAuth2ServerURL),
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", OAuth2ServerURL),
},
RedirectURL: fmt.Sprintf("%s/callback", OAuth2CallbackURL),
}
Proxy.Enabled = viper.GetBool("Proxy.Enabled")
Proxy.ProxyURL = viper.GetString("Proxy.ProxyURL")
if Proxy.ProxyURL != "" {
var err error
Proxy.ProxyURLFixed, err = url.Parse(Proxy.ProxyURL)
if err != nil {
return err
}
}
Proxy.ProxyHosts = viper.GetStringSlice("Proxy.ProxyHosts")
return nil
}

97
routers/login.go Normal file
View File

@ -0,0 +1,97 @@
package routers
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"gitea.com/gitea/pr-deployer/pkgs/proxy"
"gitea.com/gitea/pr-deployer/pkgs/settings"
"gitea.com/go-chi/session"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
func Home(w http.ResponseWriter, r *http.Request) {
sess := session.GetSession(r)
user := sess.Get("user")
oriState := "111111" // TODO: use a random
if user == nil {
sess.Set("state", oriState)
url := settings.OAuth2Conf.AuthCodeURL(oriState, oauth2.AccessTypeOffline)
fmt.Printf("Visit the URL for the auth dialog: %v", url)
http.Redirect(w, r, url, http.StatusFound)
return
}
http.Redirect(w, r, "/prs", http.StatusFound)
}
func OAuth2Callback(w http.ResponseWriter, r *http.Request) {
sess := session.GetSession(r)
oriState := sess.Get("state")
state := r.FormValue("state")
if state != oriState {
log.Error("state is not equal")
return
}
ctx := context.Background()
httpClient := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(req *http.Request) (*url.URL, error) {
return proxy.Proxy()(req)
},
},
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
code := r.FormValue("code")
tok, err := settings.OAuth2Conf.Exchange(ctx, code)
if err != nil {
log.Error(err)
return
}
fmt.Println(tok)
sess.Set("token", tok)
client := settings.OAuth2Conf.Client(ctx, tok)
var url string
if settings.ServiceType == "gitea" {
url = settings.OAuth2ServerURL + "/api/v1/user"
} else if settings.ServiceType == "github" {
url = "https://api.github.com/user"
}
resp, err := client.Get(url)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bs, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(bs))
var user = make(map[string]interface{})
err = json.NewDecoder(bytes.NewReader(bs)).Decode(&user)
if err != nil {
log.Fatal(err)
}
fmt.Println(user)
sess.Set("user", user)
http.Redirect(w, r, "/", 302)
}

View File

@ -2,57 +2,68 @@ package routers
import (
"context"
"fmt"
"net/http"
"strconv"
"gitea.com/gitea/pr-deployer/pkgs/services"
"gitea.com/gitea/pr-deployer/pkgs/settings"
"golang.org/x/oauth2"
"gitea.com/go-chi/session"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/go-github/v39/github"
log "github.com/sirupsen/logrus"
"github.com/unrolled/render"
"gitea.com/gitea/pr-deployer/pkgs/services"
"gitea.com/gitea/pr-deployer/pkgs/settings"
)
var rnd *render.Render
func Web() {
rnd = render.New()
rnd = render.New(render.Options{
IsDevelopment: true,
})
c := chi.NewRouter()
c.Use(session.Sessioner())
c.Use(middleware.Logger)
c.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("You are not login"))
})
c.Get("/", Home)
c.Get("/callback", OAuth2Callback)
c.Get("/prs", ListPRs)
c.Post("/pr/{index}/run", RunPR)
c.Post("/pr/{index}/stop", StopPR)
c.Post("/webhook", Webhook)
http.ListenAndServe(":3001", c)
}
func ListPRs(w http.ResponseWriter, r *http.Request) {
sess := session.GetSession(r)
pToken := sess.Get("token")
if pToken == nil {
http.Redirect(w, r, "/", 302)
return
}
p, _ := strconv.Atoi(r.FormValue("p"))
if p < 1 {
p = 1
}
c := github.NewClient(http.DefaultClient)
pulls, _, err := c.PullRequests.List(context.Background(), "go-gitea", "gitea", &github.PullRequestListOptions{
Sort: "updated",
Direction: "desc",
ListOptions: github.ListOptions{
Page: p,
PerPage: 100,
},
})
token := pToken.(*oauth2.Token)
pulls, err := services.GetPullRequests(token, p)
if err != nil {
log.Error(err)
return
}
user := sess.Get("user")
if err := rnd.HTML(w, http.StatusOK, "pulls", map[string]interface{}{
"pulls": pulls,
"user": user,
}); err != nil {
log.Error(err)
}
@ -65,7 +76,8 @@ func RunPR(w http.ResponseWriter, r *http.Request) {
log.Error("start failed")
return
}
if err := services.UpdateAndStartPullRequest(i); err != nil {
ctx := context.Background()
if err := services.UpdateAndStartPullRequest(ctx, i, fmt.Sprintf("refs/pull/%d/head", i)); err != nil {
log.Error("start failed")
}
}
@ -96,9 +108,16 @@ func Webhook(w http.ResponseWriter, r *http.Request) {
switch evt := event.(type) {
case *github.PullRequestEvent:
if err := services.UpdateAndStartPullRequest(*evt.Number); err != nil {
log.Error(err)
switch *evt.Action {
case "synchronize":
ctx := context.Background()
if err := services.UpdateAndStartPullRequest(ctx, *evt.Number, *evt.After); err != nil {
log.Error(err)
}
default:
log.Trace("pull request %d is %s", *evt.Number, *evt.Action)
}
default:
log.Warn("received %v type event, ignored", event)
}

1
templates/head.tmpl Normal file
View File

@ -0,0 +1 @@
<div>{{.User.Name}}</div>

View File

@ -1,5 +1,6 @@
<div>{{.user.name}}</div>
<ul>
{{range .pulls}}
<li>{{.Number}}</li>
{{end
<li>{{.Number}}: <a href="{{.HTMLURL}}">{{.Title}}</a> <button>Start Service</button></li>
{{end}}
</ul>