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) }