259 lines
5.8 KiB
Go
259 lines
5.8 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.com/gitea/pr-deployer/pkgs/settings"
|
|
|
|
"github.com/cloudflare/cloudflare-go"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/client"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func UpdateAndStartPullRequest(ctx context.Context, client Client, number int, sha string) error {
|
|
log.Trace("CheckWebhook")
|
|
if err := client.CheckWebhook(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 0 download the git
|
|
log.Trace("updateGitRepo")
|
|
|
|
newSHA, err := updateGitRepo(number, sha)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Trace("UpdateCommitStatus pending", newSHA)
|
|
// 1 send commit status
|
|
if err := client.UpdateCommitStatus(ctx, number, newSHA, statusPending, "", ""); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := updateAndStartPullRequest(ctx, client, number, newSHA); err != nil {
|
|
log.Errorf("UpdateCommitStatus %s failure: %v", newSHA, err)
|
|
if err := client.UpdateCommitStatus(ctx, number, newSHA, statusFailure, err.Error(), ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateAndStartPullRequest(ctx context.Context, client Client, number int, newSHA string) error {
|
|
log.Trace("checkAndUpdateSubDomain")
|
|
// 2 change domain
|
|
if err := checkAndUpdateSubDomain(number); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Trace("buildImage")
|
|
// 3 build
|
|
if err := buildImage(ctx, number); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Trace("runImage")
|
|
// 4 change reverse server
|
|
if err := runImage(ctx, number); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Trace("UpdateCommitStatus success", newSHA)
|
|
// 5 send commit status
|
|
if err := client.UpdateCommitStatus(ctx, number, newSHA, 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
|
|
}
|
|
|
|
// TODO: stop the old container
|
|
|
|
// start the new container
|
|
_, err = cli.ContainerExecCreate(ctx, fmt.Sprintf("%s/%s:%s", settings.RepoOwner, settings.RepoName, getImageTag(number)), types.ExecConfig{
|
|
WorkingDir: getPRDir(number),
|
|
})
|
|
return err
|
|
}
|
|
|
|
const (
|
|
statusPending = "pending"
|
|
statusSuccess = "success"
|
|
statusError = "error"
|
|
statusFailure = "failure"
|
|
)
|
|
|
|
func getPRDir(number int) string {
|
|
return filepath.Join(settings.CodeCacheDir, strconv.Itoa(number))
|
|
}
|
|
|
|
func WithDir(cmd *exec.Cmd, dir string) *exec.Cmd {
|
|
cmd.Dir = dir
|
|
return cmd
|
|
}
|
|
|
|
func updateGitRepo(number int, sha string) (string, error) {
|
|
p := getPRDir(number)
|
|
log.Trace("clone code into", p)
|
|
|
|
var local = fmt.Sprintf("pull/%d/head:pr/%d", number, number)
|
|
st, err := os.Stat(p)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
if err := exec.Command("git", "clone", settings.RepoURL, p).Run(); err != nil {
|
|
return "", fmt.Errorf("git clone: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("git", "fetch", "origin", local)
|
|
if err := WithDir(cmd, p).Run(); err != nil {
|
|
return "", errors.Wrap(err, "fetch")
|
|
}
|
|
}
|
|
return "", err
|
|
}
|
|
if !st.IsDir() {
|
|
return "", fmt.Errorf("%s is a file but not a dir", p)
|
|
}
|
|
|
|
cmd := exec.Command("git", "pull", "origin", local)
|
|
if err := WithDir(cmd, p).Run(); err != nil {
|
|
return "", errors.Wrap(err, fmt.Sprintf("checkout %s", sha))
|
|
}
|
|
|
|
var branchName = fmt.Sprintf("pr/%d", number)
|
|
var newSHA = sha
|
|
|
|
if sha != "" {
|
|
cmd := exec.Command("git", "checkout", sha)
|
|
if err := WithDir(cmd, p).Run(); err != nil {
|
|
return "", errors.Wrap(err, fmt.Sprintf("checkout %s", sha))
|
|
}
|
|
newSHA = sha
|
|
} else {
|
|
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
|
|
cmd.Dir = p
|
|
var buf bytes.Buffer
|
|
cmd.Stdout = &buf
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("rev-parse --abbrev-ref: %v", err)
|
|
}
|
|
|
|
if buf.String() != branchName {
|
|
cmd := exec.Command("git", "checkout", branchName)
|
|
if err := WithDir(cmd, p).Run(); err != nil {
|
|
return "", fmt.Errorf("git checkout %s: %v", branchName, err)
|
|
}
|
|
}
|
|
|
|
cmd = exec.Command("git", "rev-parse", "HEAD")
|
|
cmd.Dir = p
|
|
var buf2 bytes.Buffer
|
|
cmd.Stdout = &buf2
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("git rev-parse Head: %v", err)
|
|
}
|
|
newSHA = strings.TrimSpace(buf2.String())
|
|
}
|
|
|
|
return newSHA, nil
|
|
}
|
|
|
|
func getImageTag(number int) string {
|
|
return fmt.Sprintf("pr-%d", number)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var buildContext io.Reader
|
|
if _, err = cli.ImageBuild(ctx, buildContext, types.ImageBuildOptions{
|
|
Tags: []string{getImageTag(number)},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkAndUpdateSubDomain(number int) error {
|
|
api, err := cloudflare.NewWithAPIToken(settings.CloudflareToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
zoneID, err := api.ZoneIDByName("gitea.io")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var found bool
|
|
var name = fmt.Sprintf("try-pr-%d.gitea.io", number)
|
|
foo := cloudflare.DNSRecord{
|
|
Type: "name",
|
|
Name: name,
|
|
}
|
|
recs, err := api.DNSRecords(context.Background(), zoneID, foo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, r := range recs {
|
|
if name == r.Name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
// check ip address
|
|
return nil
|
|
}
|
|
|
|
asciiInput := cloudflare.DNSRecord{
|
|
Type: "A",
|
|
Name: name,
|
|
Content: settings.DomainIP,
|
|
TTL: 120,
|
|
//Priority: &priority,
|
|
//Proxied: &proxied,
|
|
}
|
|
|
|
_, err = api.CreateDNSRecord(context.Background(), zoneID, asciiInput)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func StopPullRequest(number int) error {
|
|
return nil
|
|
}
|
|
|
|
func NewClient(token *oauth2.Token) (Client, error) {
|
|
if settings.ServiceType == "github" {
|
|
return NewGithubClient(token), nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported service type: %s", settings.ServiceType)
|
|
}
|