From 056a4a963c8e7fac799c918f0b1d5dacaab69777 Mon Sep 17 00:00:00 2001 From: HeshamTB Date: Tue, 19 Sep 2023 23:47:19 +0300 Subject: [PATCH] init: working hooks --- auth.go | 24 +++++++++++++ go.mod | 3 ++ hooker.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ job.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ job_dep.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ logging.go | 28 +++++++++++++++ runner.go | 61 ++++++++++++++++++++++++++++++++ 7 files changed, 396 insertions(+) create mode 100644 auth.go create mode 100644 go.mod create mode 100644 hooker.go create mode 100644 job.go create mode 100644 job_dep.go create mode 100644 logging.go create mode 100644 runner.go diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..5b7c8a4 --- /dev/null +++ b/auth.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "net/http" +) + +type AuthHandler struct { + Handler http.Handler +} + +func (l *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + + AuthHeader := r.Header.Get("Authorization") + if AuthHeader != task.Auth { + log.Printf("Dropping request from %s.", r.RemoteAddr) + return // drop + } + l.Handler.ServeHTTP(w, r) +} + +func NewAuth(handler http.Handler) AuthHandler { + return AuthHandler{handler} +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0bbeadf --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.hbanafa.com/hesham/hooker + +go 1.21.1 diff --git a/hooker.go b/hooker.go new file mode 100644 index 0000000..7527d34 --- /dev/null +++ b/hooker.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "strconv" + "sync" + "time" +) + +/* + Listen for git web hooks and then execute a script. +*/ + +type TaskDefErr struct { + message string +} + +func (e *TaskDefErr) Error() string { + return e.message +} + +type TaskManifest struct { + Owner string + RepoID uint64 + Auth string + Command string // Path to executable + Lock *sync.Mutex +} + +var httpServer http.Server +var task TaskManifest + +func InitHTTPHandler(handler *http.ServeMux) *LoggingHTTPHandler { + + auth := NewAuth(handler) + loggingHandler := NewLogger(&auth) + return &loggingHandler +} + +// Try to get an env var, if not set, panic or fatal +func getEnv(name string) string { + varVal := os.Getenv(name) + if varVal == "" { + panic(fmt.Sprintf("required %s env variable is not set", name)) + } + return varVal +} + +func InitTask() TaskManifest { + + RepoID := getEnv("REPO_ID") + repoID, err := strconv.ParseUint(RepoID, 10, 64) + if err != nil { + panic("REPO_ID is not a valid int") + } + + return TaskManifest{ + Command: getEnv("CMD"), + Owner: getEnv("OWNER"), + Auth: getEnv("AUTH"), + RepoID: repoID, + Lock: &sync.Mutex{}, + } +} + +func init() { + TAG := "[ init ]" + l := func(msg string) { + log.Println(TAG, msg) + } + l("starting") + + l("Validating Task") + task = InitTask() + + l("Registering handlers") + mux := http.NewServeMux() + mux.HandleFunc("/", handleRoot) + + handler := InitHTTPHandler(mux) + httpServer = http.Server{ + ReadTimeout: time.Second * 5, + WriteTimeout: time.Second * 5, + Addr: ":4184", + Handler: handler, + } + +} + +func main() { + + log.Println("Task", task) + log.Printf("Listening on %s", httpServer.Addr) + log.Fatal(httpServer.ListenAndServe()) + log.Println("Server Stopped") + +} + diff --git a/job.go b/job.go new file mode 100644 index 0000000..e8542ea --- /dev/null +++ b/job.go @@ -0,0 +1,94 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" +) + +type Owner struct { + Login string `json:"login"` + Username string `json:"username"` +} + +type Repo struct { + Name string `json:"name"` + ID uint64 `json:"id"` + Owner Owner `json:"owner"` +} + +type JsonRoot struct { + Repo Repo `json:"repository"` + +} + +func handleRoot(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + return // Drop + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Could not read request body", http.StatusBadRequest) + return + } + + var JsonRoot JsonRoot + err = json.Unmarshal(body, &JsonRoot); if err != nil { + log.Println("Can not Unmarshal body json") + http.Error(w,"Can not Unmarshal body json", http.StatusBadRequest) + return + } + fmt.Printf("JsonRoot: %v\n", JsonRoot) + + if JsonRoot.Repo.ID != task.RepoID { + log.Printf( + "Repo ID sent: %d, Expecting: %d.", + JsonRoot.Repo.ID, + task.RepoID, + ) + w.WriteHeader(http.StatusNotFound) + return + } + + if JsonRoot.Repo.Owner.Login != task.Owner { + log.Printf( + "Owner sent: %s, Expecting: %s", + JsonRoot.Repo.Owner.Login, + task.Owner, + ) + w.WriteHeader(http.StatusNotFound) + return + } + + DeliveryUUID := findXDeliveryUUID(r) + if DeliveryUUID == "" { + log.Println("Could not find Delivery UUID") + w.WriteHeader(http.StatusBadRequest) + return + } + + log.Println("Starting New Job", DeliveryUUID) + go RunJob(&task, DeliveryUUID, log.Default()) + +} + +func findXDeliveryUUID(r *http.Request) string { + + var headerVal string + + possibleHeaders := []string{ + "X-Gitea-Delivery", + "X-GitHub-Delivery", + "X-Gogs-Delivery", + } + + for _, head := range possibleHeaders { + headerVal = r.Header.Get(head) + if headerVal != "" { break } + } + return headerVal + +} diff --git a/job_dep.go b/job_dep.go new file mode 100644 index 0000000..251fd1c --- /dev/null +++ b/job_dep.go @@ -0,0 +1,85 @@ +package main + +/* + This is scrapped for later use. + Now make a simple, one task runner with params + defined as env vars. +*/ +import ( + "os/exec" + "time" +) + + +type JobStatus uint64 + +const ( + NEW = iota + RUNNING + FINISHED + FAILED +) + +type Job interface { + Start() + Result() string +} + +// Tasks are defined via config and are used to run jobs +type Task struct { + RepoFullName string + Owner string + Secret string + command exec.Cmd +} + +type ShellCommandJob struct { + JobStatus + Task + UUID string + ExitCode uint8 + stdout string + err *error + TimeCreated time.Time + TimeStarted time.Time + TimeFinished time.Time // This is redundant but keep it for now + TimeConsumed time.Duration +} + +// TODO: Replace with task method to generate jobs +func NewShellJob(t Task, uuid string) *ShellCommandJob { + + timeNow := time.Now().UTC() + job := ShellCommandJob{ + JobStatus: NEW, + Task: t, + UUID: uuid, + TimeCreated: timeNow, + } + return &job +} + +func (j *ShellCommandJob) Start() { + j.TimeStarted = time.Now().UTC() + stdout, err := j.command.Output() + if err != nil { + j.JobStatus = FAILED + } + j.TimeFinished = time.Now().UTC() + j.TimeConsumed = time.Since(j.TimeStarted) + j.stdout = string(stdout) +} + +func (j *ShellCommandJob) Result() string { + return j.stdout +} + +func (j *ShellCommandJob) String() string { + switch j.JobStatus { + case 0: return "NEW" + case 1: return "RUNNING" + case 2: return "FINISHED" + case 3: return "FAILD" + default: return "UNDEFINED" + } +} diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..b4248e4 --- /dev/null +++ b/logging.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "time" +) + +type LoggingHTTPHandler struct { + Handler http.Handler +} + +func (l *LoggingHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + + t1 := time.Now().UTC() + + l.Handler.ServeHTTP(w, r) + + log.Println( + fmt.Sprintf("%s %s %s %v", r.RemoteAddr, r.Method, r.URL.Path, time.Since(t1)), + ) + +} + +func NewLogger(handler http.Handler) LoggingHTTPHandler { + return LoggingHTTPHandler{handler} +} diff --git a/runner.go b/runner.go new file mode 100644 index 0000000..d761002 --- /dev/null +++ b/runner.go @@ -0,0 +1,61 @@ +package main + +import ( + "bytes" + "io" + "log" + "os/exec" + "strings" +) + +// Starts the task report it's exit status +// To Logger +func RunJob(task *TaskManifest, deliveryUUID string, logger *log.Logger) { + + var stderrBuf, stdoutBuf bytes.Buffer + + execuatable, args := CommandStrtoProgArgs(task.Command) + + cmd := exec.Command(execuatable, args...) + + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + + + task.Lock.Lock() + err := cmd.Run() + task.Lock.Unlock() + if err != nil { + logger.Println("Exec reported error", err.Error()) + return + } + + stdout, err := io.ReadAll(&stdoutBuf) + if err != nil { + logger.Printf("Could not read stdout from command for job %s\n", deliveryUUID) + return + } + stderr, err := io.ReadAll(&stderrBuf) + if err != nil { + logger.Printf("Could not read stderr from command for job %s\n", deliveryUUID) + return + } + + logger.Printf("stdout: %v", string(stdout)) + logger.Printf("stderr: %v", string(stderr)) + logger.Printf("Job: %s exited with code: %d", deliveryUUID, cmd.ProcessState.ExitCode()) + + +} + +// Seperate executable from args in command string +func CommandStrtoProgArgs(cmd string) (Execuatbale string, Args []string) { + tokens := strings.Split(cmd, " ") + + if len(tokens) == 1 { + return tokens[0], nil + } + + return tokens[0], tokens[1:] +} +