pr-deployer/pkgs/services/services.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)
}