Feat: Remove Requirement for Dotfile (#84)

This MR removes the requirement for a dotfile (the dotfile is now optional and will override the configuration provided via environment variables). The requirement for providing a project ID is also eliminated, by parsing the namespace and project name from the SSH or HTTPS remote, and then using that to query Gitlab for a matching project.
This commit is contained in:
Harrison (Harry) Cramer
2023-11-11 23:51:11 -05:00
committed by GitHub
parent 38df51bfbc
commit 80b597e56a
8 changed files with 216 additions and 145 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/http/httputil"
"os"
@@ -18,7 +19,6 @@ type Client struct {
mergeId int
gitlabInstance string
authToken string
logPath string
git *gitlab.Client
}
@@ -27,85 +27,32 @@ type DebugSettings struct {
GoResponse bool `json:"go_response"`
}
var requestLogger retryablehttp.RequestLogHook = func(l retryablehttp.Logger, r *http.Request, i int) {
logPath := os.Args[len(os.Args)-1]
/* This will parse and validate the project settings and then initialize the Gitlab client */
func (c *Client) initGitlabClient() error {
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
defer file.Close()
token := r.Header.Get("Private-Token")
r.Header.Set("Private-Token", "REDACTED")
res, err := httputil.DumpRequest(r, true)
if err != nil {
panic(err)
}
r.Header.Set("Private-Token", token)
_, err = file.Write([]byte("\n-- REQUEST --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}
var responseLogger retryablehttp.ResponseLogHook = func(l retryablehttp.Logger, response *http.Response) {
logPath := os.Args[len(os.Args)-1]
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
defer file.Close()
res, err := httputil.DumpResponse(response, true)
if err != nil {
panic(err)
if len(os.Args) < 6 {
return errors.New("Must provide gitlab url, port, auth token, debug settings, and log path")
}
_, err = file.Write([]byte("\n-- RESPONSE --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}
/* This will initialize the client with the token and check for the basic project ID and command arguments */
func (c *Client) init(branchName string) error {
if len(os.Args) < 5 {
return errors.New("Must provide project ID, gitlab instance, port, and auth token!")
gitlabInstance := os.Args[1]
if gitlabInstance == "" {
return errors.New("GitLab instance URL cannot be empty")
}
projectId := os.Args[1]
gitlabInstance := os.Args[2]
authToken := os.Args[4]
debugSettings := os.Args[5]
authToken := os.Args[3]
if authToken == "" {
return errors.New("Auth token cannot be empty")
}
/* Parse debug settings and initialize logger handlers */
debugSettings := os.Args[4]
var debugObject DebugSettings
err := json.Unmarshal([]byte(debugSettings), &debugObject)
if err != nil {
return fmt.Errorf("Could not parse debug settings: %w, %s", err, debugSettings)
}
logPath := os.Args[len(os.Args)-1]
if projectId == "" {
return errors.New("Project ID cannot be empty")
}
if gitlabInstance == "" {
return errors.New("GitLab instance URL cannot be empty")
}
if authToken == "" {
return errors.New("Auth token cannot be empty")
}
c.gitlabInstance = gitlabInstance
c.projectId = projectId
c.authToken = authToken
c.logPath = logPath
var apiCustUrl = fmt.Sprintf(c.gitlabInstance + "/api/v4")
var apiCustUrl = fmt.Sprintf(gitlabInstance + "/api/v4")
gitlabOptions := []gitlab.ClientOptionFunc{
gitlab.WithBaseURL(apiCustUrl),
@@ -125,13 +72,40 @@ func (c *Client) init(branchName string) error {
return fmt.Errorf("Failed to create client: %v", err)
}
c.gitlabInstance = gitlabInstance
c.authToken = authToken
c.git = git
return nil
}
/* This will fetch the project ID and merge request ID using the client */
func (c *Client) initProjectSettings(remoteUrl string, namespace string, projectName string, branchName string) error {
idStr := namespace + "/" + projectName
opt := gitlab.GetProjectOptions{}
project, _, err := c.git.Projects.GetProject(idStr, &opt)
if err != nil {
return fmt.Errorf(fmt.Sprintf("Error getting project at %s", remoteUrl), err)
}
if project == nil {
return fmt.Errorf(fmt.Sprintf("Could not find project at %s", remoteUrl), err)
}
if project == nil {
return fmt.Errorf("No projects you are a member of contained remote URL %s", remoteUrl)
}
c.projectId = fmt.Sprint(project.ID)
options := gitlab.ListProjectMergeRequestsOptions{
Scope: gitlab.String("all"),
State: gitlab.String("opened"),
SourceBranch: &branchName,
}
mergeRequests, _, err := git.MergeRequests.ListProjectMergeRequests(c.projectId, &options)
mergeRequests, _, err := c.git.MergeRequests.ListProjectMergeRequests(c.projectId, &options)
if err != nil {
return fmt.Errorf("Failed to list merge requests: %w", err)
}
@@ -147,7 +121,6 @@ func (c *Client) init(branchName string) error {
}
c.mergeId = mergeIdInt
c.git = git
return nil
}
@@ -165,3 +138,54 @@ func (c *Client) handleError(w http.ResponseWriter, err error, message string, s
c.handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}
var requestLogger retryablehttp.RequestLogHook = func(l retryablehttp.Logger, r *http.Request, i int) {
file := openLogFile()
defer file.Close()
token := r.Header.Get("Private-Token")
r.Header.Set("Private-Token", "REDACTED")
res, err := httputil.DumpRequest(r, true)
if err != nil {
log.Fatalf("Error dumping request: %v", err)
os.Exit(1)
}
r.Header.Set("Private-Token", token)
_, err = file.Write([]byte("\n-- REQUEST --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}
var responseLogger retryablehttp.ResponseLogHook = func(l retryablehttp.Logger, response *http.Response) {
file := openLogFile()
defer file.Close()
res, err := httputil.DumpResponse(response, true)
if err != nil {
log.Fatalf("Error dumping response: %v", err)
os.Exit(1)
}
_, err = file.Write([]byte("\n-- RESPONSE --\n")) //nolint:all
_, err = file.Write(res) //nolint:all
_, err = file.Write([]byte("\n")) //nolint:all
}
func openLogFile() *os.File {
logFile := os.Args[len(os.Args)-1]
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
if os.IsNotExist(err) {
log.Printf("Log file %s does not exist", logFile)
} else if os.IsPermission(err) {
log.Printf("Permission denied for log file %s", logFile)
} else {
log.Printf("Error opening log file %s: %v", logFile, err)
}
os.Exit(1)
}
return file
}

67
cmd/git.go Normal file
View File

@@ -0,0 +1,67 @@
package main
import (
"fmt"
"os/exec"
"regexp"
"strings"
)
/*
Extracts information about the current repository and returns
it to the client for initialization. The current directory must be a valid
Gitlab project and the branch must be a feature branch
*/
func ExtractGitInfo() (string, string, string, string, error) {
url, err := getProjectUrl()
if err != nil {
return "", "", "", "", fmt.Errorf("Could not get project Url: %v", err)
}
re := regexp.MustCompile(`(?:[:\/])([^\/]+)\/([^\/]+)\.git$`)
matches := re.FindStringSubmatch(url)
if len(matches) != 3 {
return "", "", "", "", fmt.Errorf("Invalid Git URL format: %s", url)
}
namespace := matches[1]
projectName := matches[2]
branch, err := getCurrentBranch()
if err != nil {
return "", "", "", "", fmt.Errorf("Failed to get current branch: %v", err)
}
return url, namespace, projectName, branch, nil
}
/* Gets the current branch */
func getCurrentBranch() (res string, e error) {
gitCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := gitCmd.Output()
if err != nil {
return "", fmt.Errorf("Error running git rev-parse: %w", err)
}
branchName := strings.TrimSpace(string(output))
if branchName == "main" || branchName == "master" {
return "", fmt.Errorf("Cannot run on %s branch", branchName)
}
return branchName, nil
}
/* Gets the project SSH or HTTPS url */
func getProjectUrl() (res string, e error) {
cmd := exec.Command("git", "remote", "get-url", "origin")
url, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("Could not get origin remote")
}
return strings.TrimSpace(string(url)), nil
}

View File

@@ -7,29 +7,24 @@ import (
"net"
"net/http"
"os"
"os/exec"
"strings"
"time"
)
func main() {
branchName, err := getCurrentBranch()
url, namespace, projectName, branchName, err := ExtractGitInfo()
if err != nil {
log.Fatalf("Failure: Failed to get current branch: %v", err)
log.Fatalf("Failed to get git namespace, project, branch, or url: %v", err)
}
if branchName == "main" || branchName == "master" {
log.Fatalf("Cannot run on %s branch", branchName)
}
/* Initialize Gitlab client */
var c Client
if err := c.init(branchName); err != nil {
if err := c.initGitlabClient(); err != nil {
log.Fatalf("Failed to initialize Gitlab client: %v", err)
}
if err := c.initProjectSettings(url, namespace, projectName, branchName); err != nil {
log.Fatalf("Failed to initialize project settings: %v", err)
}
m := http.NewServeMux()
m.Handle("/ping", http.HandlerFunc(PingHandler))
m.Handle("/mr/summary", withGitlabContext(http.HandlerFunc(SummaryHandler), c))
@@ -47,7 +42,7 @@ func main() {
m.Handle("/pipeline", withGitlabContext(http.HandlerFunc(PipelineHandler), c))
m.Handle("/job", withGitlabContext(http.HandlerFunc(JobHandler), c))
port := os.Args[3]
port := os.Args[2]
if port == "" {
// port was not specified
port = "0"
@@ -59,7 +54,7 @@ func main() {
fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
os.Exit(1)
}
listner_port := listener.Addr().(*net.TCPAddr).Port
listenerPort := listener.Addr().(*net.TCPAddr).Port
errCh := make(chan error)
go func() {
@@ -69,10 +64,10 @@ func main() {
go func() {
for i := 0; i < 10; i++ {
resp, err := http.Get("http://localhost:" + fmt.Sprintf("%d", listner_port) + "/ping")
resp, err := http.Get("http://localhost:" + fmt.Sprintf("%d", listenerPort) + "/ping")
if resp.StatusCode == 200 && err == nil {
/* This print is detected by the Lua code and used to fetch project information */
fmt.Println("Server started on port: ", listner_port)
fmt.Println("Server started on port: ", listenerPort)
return
}
// Wait for healthcheck to pass - at most 1 sec.
@@ -85,7 +80,11 @@ func main() {
fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
os.Exit(1)
}
}
func PingHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "pong")
}
func withGitlabContext(next http.HandlerFunc, c Client) http.Handler {
@@ -94,19 +93,3 @@ func withGitlabContext(next http.HandlerFunc, c Client) http.Handler {
next.ServeHTTP(w, r.WithContext(ctx))
})
}
/* Gets the current branch */
func getCurrentBranch() (res string, e error) {
gitCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := gitCmd.Output()
if err != nil {
return "", fmt.Errorf("Error running git rev-parse: %w", err)
}
return strings.TrimSpace(string(output)), nil
}
func PingHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "pong")
}