diff --git a/README.md b/README.md index 19f0cae..56fc6b2 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,22 @@ The application reads Gitea credentials from the following files: - `/etc/assist-secret/gitea-username` for the username. - `/etc/assist-secret/gitea-password` for the password. -Ensure these files are present and contain the necessary credentials for accessing Gitea repositories. +The application expects a Bearer Token for use of gitea functionality which is supplied by: +- `/etc/assist-secret/assist-token` for the assist auth token. + +Ensure these files are present and contain the necessary credentials for accessing Gitea repositories. These can be created by using the mk_passwd.py script ## Endpoints - `/onPush`: Endpoint to receive Gitea push webhooks. - `/readiness`: A readiness endpoint that returns 200 OK, indicating the service is ready to handle requests. - `/liveness`: A liveness endpoint that returns 200 OK, indicating the service is alive and healthy. +- `/users`: Endpoint to handle users in Gitea instance +- `/users/ssh`: Endpoint to manage ssh keys for Gitea +- `/repos`: Endpoint to manage repo list + +An example curl command needed to hit any of the endpoints is: +`curl -H "Authorization: Bearer " -X GET "http://:9000/users?username="` ## Running the Application @@ -49,7 +58,7 @@ However, you can specify custom values for both. ### Usage -- **Default (random password, `gitea_admin` username):** +- **Default (random password, `gitea_admin` username and `assist-token`):** ``` ./mk_passwd.py ``` @@ -64,9 +73,9 @@ However, you can specify custom values for both. ./mk_passwd.py --username ``` -- **Specify both password and username:** +- **Specify both password, username and token:** ``` - ./mk_passwd.py --password --username + ./mk_passwd.py --password --username --token ``` ### Behavior diff --git a/error/errorapi.go b/error/errorapi.go new file mode 100644 index 0000000..82eadaf --- /dev/null +++ b/error/errorapi.go @@ -0,0 +1,61 @@ +package errorapi + +import ( + "errors" + "fmt" + "log" + "net/http" + "strings" +) + +type APIError struct { + error + status int +} + +func (e APIError) Error() string { + msg := e.error.Error() + // The join method while wrapping concatenates error messages with newlines. Replace newlines with spaces + return strings.Replace(msg, "\n", " ", -1) +} + +func (e APIError) Compare(err *APIError) bool { + return errors.Is(e.error, err.error) +} + +var ( + // A list of general error of type APIError + ErrBadRequest = &APIError{error: errors.New("bad request"), status: http.StatusBadRequest} + ErrNotFound = &APIError{error: errors.New("not found"), status: http.StatusNotFound} + ErrInternalServerError = &APIError{error: errors.New("internal server error"), status: http.StatusInternalServerError} + ErrRequestReadError = &APIError{error: errors.New("error reading request body"), status: http.StatusBadRequest} + ErrResponseReadError = &APIError{error: errors.New("error reading response body"), status: http.StatusBadRequest} + ErrRequestParseError = &APIError{error: errors.New("request parse error"), status: http.StatusBadRequest} + ErrMethodNotAllowed = &APIError{error: errors.New("method not allowed"), status: http.StatusMethodNotAllowed} + ErrGiteaConnectError = &APIError{error: errors.New("error connecting to gitea"), status: http.StatusBadRequest} + ErrUnauthorized = &APIError{error: errors.New("unauthorized attempt to login"), status: http.StatusUnauthorized} + // Making a slice for all predefined errors for ease of comparison in HandleError below + allErrors = []*APIError{ErrBadRequest, ErrNotFound, ErrInternalServerError, ErrRequestReadError, ErrResponseReadError, ErrMethodNotAllowed, ErrRequestParseError, ErrMethodNotAllowed, ErrGiteaConnectError, ErrUnauthorized} +) + +// This function provides capability to "modify" the message of an existing error +// but still keep the original type when comparing +// For example: +// repoError := WrapError(ErrNotFound, "Repo") :: Creates a new error specific to repo +// But now, repoError.Compare(ErrNotFound) will return true because of the Join method of errors package +func WrapError(e *APIError, msg string) *APIError { + err := errors.Join(fmt.Errorf("%v", msg), e.error) + return &APIError{error: err, status: e.status} +} + +func HandleError(w http.ResponseWriter, err *APIError) { + for _, sperr := range allErrors { + if err.Compare(sperr) { + http.Error(w, err.Error(), err.status) + log.Printf("ERROR: %s STATUS: %d\n", err.Error(), err.status) + return + } + } + http.Error(w, ErrInternalServerError.Error(), ErrInternalServerError.status) + log.Printf("Unknown error occured %v", err) +} diff --git a/main.go b/main.go index 1a1fe81..c7849dc 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,19 @@ package main import ( "archive/zip" "bytes" + "context" "encoding/json" "fmt" + errorapi "gitea_assist/error" "io" - "io/ioutil" "log" "net/http" "os" + "os/signal" "path/filepath" "strings" "sync/atomic" + "syscall" "time" api "code.gitea.io/gitea/modules/structs" @@ -124,11 +127,13 @@ type AtomicCounter struct { var access *GiteaAccess var forkCounter *AtomicCounter var fullname string +var assistToken string func init() { access, _ = getAccess() fullname, _ = getFullname() forkCounter = &AtomicCounter{} + assistToken = assistAdminToken() } // Next returns the next number in sequence @@ -136,6 +141,56 @@ func (ac *AtomicCounter) Next() int64 { return atomic.AddInt64(&ac.val, 1) } +// Bare minimum auth middleware in lieu of major restructuring. +// Assumes we are reading a K8S secret mounted with the other secrets +// read in by the init() func at /etc/assist-secret/assist-token +// This can be prepopulated by the mk_password.py file. +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Ensure that we indicate authorization may vary + w.Header().Add("Vary", "Authorization") + + // Returns "" empty string if nothing is found. + authorizationHeader := r.Header.Get("Authorization") + + // If no auth header, set user as anonymous + if authorizationHeader == "" { + w.Header().Set("WWW-Authenticate", "Bearer") + message := "invalid or missing auth token" + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrUnauthorized, message)) + return + } + + headerParts := strings.Split(authorizationHeader, " ") + if len(headerParts) != 2 || headerParts[0] != "Bearer" { + w.Header().Set("WWW-Authenticate", "Bearer") + message := "invalid or missing auth token" + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrUnauthorized, message)) + return + } + + token := headerParts[1] + + // Trim the whitespace from file + if assistToken != strings.TrimSpace(token) { + w.Header().Set("WWW-Authenticate", "Bearer") + message := "invalid or missing auth token" + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrUnauthorized, message)) + return + } + next.ServeHTTP(w, r) + }) +} + +func assistAdminToken() string { + admin, err := os.ReadFile("/etc/assist-secret/assist-token") + if err != nil { + log.Fatalf("init()-Error reading assist-token file: %v", err) + } + assistToken := strings.TrimSpace(string(admin)) + return assistToken +} + func getAccess() (*GiteaAccess, error) { var access *GiteaAccess @@ -176,12 +231,11 @@ func getFullname() (string, error) { } -func downloadPathFromZip(zipBytes []byte, path string) ([]byte, error) { +func downloadPathFromZip(zipBytes []byte, path string) ([]byte, *errorapi.APIError) { reader := bytes.NewReader(zipBytes) zipReader, err := zip.NewReader(reader, int64(len(zipBytes))) if err != nil { - log.Printf("unable to parse zipfile from bytes for path %v", path) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("unable to parse zipfile from bytes for path %v", path)) } buf := new(bytes.Buffer) zipWriter := zip.NewWriter(buf) @@ -217,8 +271,7 @@ func downloadPathFromZip(zipBytes []byte, path string) ([]byte, error) { } if _, err := io.Copy(fileWriter, fileReader); err != nil { - log.Printf("failed to write %v to new zip file", zFile.Name) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to write %v to new zip file", zFile.Name)) } } @@ -291,21 +344,20 @@ func deleteTokenForUser(giteaBaseURL, adminUsername, adminPassword, targetUser, // basic authentication with the provided username and password. The // function returns a slice of api.Repository representing the forks and // an error if there's an issue with the HTTP request or response parsing. -func findForks(repoURL, username, password string) ([]api.Repository, error) { +func findForks(repoURL, username, password string) ([]api.Repository, *errorapi.APIError) { var forks []api.Repository client := &http.Client{} req, err := http.NewRequest("GET", repoURL+"/forks", nil) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(username), string(password)) resp, err := client.Do(req) if err != nil { - log.Printf("could not retrieve forks %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() @@ -313,10 +365,8 @@ func findForks(repoURL, username, password string) ([]api.Repository, error) { err = dec.Decode(&forks) if err != nil { - log.Printf("unable to parse response from %s/forks %v", repoURL, err) - body, err := io.ReadAll(resp.Body) - log.Printf("body:\n %s", body) - return nil, err + _, err := io.ReadAll(resp.Body) + return nil, errorapi.WrapError(errorapi.ErrRequestParseError, fmt.Sprintf("unable to parse response from %s/forks %v", repoURL, err)) } return forks, nil @@ -326,20 +376,19 @@ func getRemoteUrlFromRepo(repo *api.Repository) string { return repo.SSHURL } -func getRemoteUrl(giteaBaseURL, adminUsername, adminPassword, owner string, repo string) (string, error) { +func getRemoteUrl(giteaBaseURL, adminUsername, adminPassword, owner string, repo string) (string, *errorapi.APIError) { client := &http.Client{} repoURL := fmt.Sprintf("%s/repos/%s/%s", giteaBaseURL, owner, repo) req, err := http.NewRequest("GET", repoURL, nil) if err != nil { - return "", err + return "", errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := client.Do(req) if err != nil { - log.Printf("could not retrieve remote url %v", err) - return "", err + return "", errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("could not retrieve remote url %v", err)) } defer resp.Body.Close() @@ -347,7 +396,7 @@ func getRemoteUrl(giteaBaseURL, adminUsername, adminPassword, owner string, repo var responseError map[string]interface{} json.NewDecoder(resp.Body).Decode(&responseError) - return "", fmt.Errorf("failed to retrieve remote url; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + return "", errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to retrieve remote url; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"])) } var repository api.Repository @@ -411,7 +460,7 @@ func renameRepo(giteaBaseURL, adminUsername, adminPassword, owner, currentRepoNa return nil } -func createTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, description string) error { +func createTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, description string) *errorapi.APIError { reqURL := fmt.Sprintf("%s/orgs/%s/teams", giteaBaseURL, orgName) // Define team details @@ -428,7 +477,7 @@ func createTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, d req, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonData)) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.Header.Add("Content-Type", "application/json") @@ -436,12 +485,12 @@ func createTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, d resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("failed to create team; HTTP status code: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to create team; HTTP status code: %d", resp.StatusCode)) } return nil @@ -474,16 +523,16 @@ func getTeamID(giteaBaseURL, adminUsername, adminPassword, orgName, teamName str return -1, fmt.Errorf("team %s not found in organization %s", teamName, orgName) } -func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, userName string) error { +func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, userName string) *errorapi.APIError { teamID, err := getTeamID(giteaBaseURL, adminUsername, adminPassword, orgName, teamName) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } reqURL := fmt.Sprintf("%s/teams/%d/members/%s", giteaBaseURL, teamID, userName) req, err := http.NewRequest("PUT", reqURL, bytes.NewBuffer(nil)) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.Header.Add("Content-Type", "application/json") @@ -491,30 +540,29 @@ func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { var responseError map[string]interface{} - log.Printf("%v %v", resp.StatusCode, reqURL) json.NewDecoder(resp.Body).Decode(&responseError) - return fmt.Errorf("failed to add user to team; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to add user to team; HTTP status code: %d, message: %s, url: %v", resp.StatusCode, responseError["message"], reqURL)) } return nil } -func deleteUserFromTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, userName string) error { +func deleteUserFromTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, userName string) *errorapi.APIError { teamID, err := getTeamID(giteaBaseURL, adminUsername, adminPassword, orgName, teamName) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } reqURL := fmt.Sprintf("%s/teams/%d/members/%s", giteaBaseURL, teamID, userName) req, err := http.NewRequest("DELETE", reqURL, bytes.NewBuffer(nil)) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.Header.Add("Content-Type", "application/json") @@ -522,7 +570,7 @@ func deleteUserFromTeam(giteaBaseURL, adminUsername, adminPassword, orgName, tea resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() @@ -530,7 +578,7 @@ func deleteUserFromTeam(giteaBaseURL, adminUsername, adminPassword, orgName, tea var responseError map[string]interface{} log.Printf("%v %v", resp.StatusCode, reqURL) json.NewDecoder(resp.Body).Decode(&responseError) - return fmt.Errorf("failed to delete user from team; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to delete user from team; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"])) } return nil @@ -896,114 +944,7 @@ func processMerge(mc *MergeContext, filePatches []diff.FilePatch) error { return applyChanges(mc, filteredPatches) } -// processPushEvent handles a push event from a Git repository. Given the event's -// payload and authentication credentials, the function performs several tasks: -// - Identifies the repository associated with the push. -// - Clears any previous repository data from a temporary directory. -// - Locates all forks of the repository and processes each one by: -// a. Cloning the upstream and fork repositories. -// b. Creating a merge context for the two repositories. -// c. Computing and applying diffs between the repositories. -// d. Pushing merged changes to the fork. -// -// Error situations, such as cloning failures or merge issues, are logged. -func processPushEvent(pushEvent *api.PushPayload, access *GiteaAccess) { - var forkIsEmpty bool = false - - // 1. Get the repository related to the push event - languagesURL := pushEvent.Repo.LanguagesURL - repoURL := strings.ReplaceAll(languagesURL, "/languages", "") - log.Printf("processing push event on repo with URL %s", repoURL) - - err := os.RemoveAll("/tmp/repos") - if err != nil { - log.Printf("failed to clean work directory") - return - } - - if forks, err := findForks(repoURL, access.Username, access.Password); err == nil { - var pushRepo *git.Repository - - for _, fork := range forks { - log.Printf("found fork %s", fork.Owner.UserName+"/"+fork.Name) - if pushRepo == nil { - pushRepo, err = cloneRepoIntoDir("/tmp/repos/", "upstream/"+pushEvent.Repo.Name, pushEvent.Repo.CloneURL, false) - if err != nil { - log.Printf("Failed to clone the upstream repository: %v", err) - return - } - } - forkRepo, err := cloneRepoIntoDir("/tmp/repos/", fork.Owner.UserName+"/"+fork.Name, fork.CloneURL, true) - if err != nil { - log.Printf("Failed to clone the fork repository: %v", err) - continue - } - - // This happens when the fork is empty, so have to initilize it locally - if forkRepo == nil { - if forkRepo, err = InitRepoWithRemote("/tmp/repos/"+fork.Owner.UserName+"/"+fork.Name, fork.CloneURL, pushEvent.Branch()); err != nil { - log.Printf("Failed to initialize blank fork repository: %v", err) - continue - } - forkIsEmpty = true - } - - mc := &MergeContext{ - Upstream: pushRepo, - UpstreamCloneURL: pushEvent.Repo.CloneURL, - UpstreamName: "upstream/" + pushEvent.Repo.Name, - UpstreamBranch: pushEvent.Branch(), - Fork: forkRepo, - ForkCloneURL: fork.CloneURL, - ForkName: fork.Owner.UserName + "/" + fork.Name, - ForkBranch: pushEvent.Branch(), - ForkIsEmpty: forkIsEmpty, - } - if patches, err := getDiffBetweenUpstreamAndFork(mc); err == nil { - var filePatches []diff.FilePatch - - for _, patch := range patches { - filePatches = append(filePatches, patch.FilePatches()...) - } - if err = processMerge(mc, filePatches); err == nil { - pushFork(mc, access) - } else { - log.Printf("failed to process merge of %s into %s: %v", mc.UpstreamName, mc.ForkName, err) - } - } else { - log.Printf("failed to compute upstream and fork diff: %v", err) - } - } - } -} - -// webhookHandler handles incoming webhook requests, specifically for push events. -// The function reads the request body, parses the push event, and processes it. -// Any errors in reading or parsing are logged and result in a bad request response. -// After processing the push event, a log confirmation is made. -func webhookHandler(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Error reading body: %v", err) - http.Error(w, "can't read body", http.StatusBadRequest) - return - } - - pushEvent, err := api.ParsePushHook(body) - - if err != nil { - log.Printf("Error parsing body: %v", err) - http.Error(w, "can't read body", http.StatusBadRequest) - return - } - - // Process the push event, including finding forks and pulling changes - processPushEvent(pushEvent, access) - - log.Printf("OK") -} - -func createUser(giteaBaseURL, adminUsername, adminPassword, username, password, email string) (bool, error) { +func createUser(giteaBaseURL, adminUsername, adminPassword, username, password, email string) (bool, *errorapi.APIError) { /* user := giteaAPI.CreateUserOption{ Username: username, @@ -1027,20 +968,22 @@ func createUser(giteaBaseURL, adminUsername, adminPassword, username, password, jsonData, _ := json.Marshal(user) - req, _ := http.NewRequest("POST", giteaBaseURL+"/admin/users", bytes.NewBuffer(jsonData)) + req, err := http.NewRequest("POST", giteaBaseURL+"/admin/users", bytes.NewBuffer(jsonData)) + if err != nil { + return false, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) + } //req.Header.Add("Authorization", "token "+token) req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return false, err + return false, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) - log.Println("Failed to create user:", string(body)) - return false, nil + return false, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("create user returned unexpected status: %s", string(body))) } return true, nil } @@ -1050,19 +993,19 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.ErrRequestReadError) return } var options CreateUserOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } if options.Username == "" || options.Password == "" || options.Email == "" { - http.Error(w, "Username password, and email must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Username, password, and email must be provided")) return } @@ -1072,16 +1015,14 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("User created successfully")) } else { - http.Error(w, "User creation failed", http.StatusBadRequest) - if err != nil { - log.Printf("User creation failed %v", err) - } else { - log.Printf("User creation failed") + if err == nil { + err = errorapi.WrapError(errorapi.ErrInternalServerError, "User creation failed ") } + errorapi.HandleError(w, err) } } -func deleteUser(giteaBaseURL, adminUsername, adminPassword, username string, purge bool) (bool, error) { +func deleteUser(giteaBaseURL, adminUsername, adminPassword, username string, purge bool) (bool, *errorapi.APIError) { url := fmt.Sprintf("%s/admin/users/%s?purge=%t", giteaBaseURL, username, purge) req, _ := http.NewRequest("DELETE", url, nil) @@ -1089,14 +1030,15 @@ func deleteUser(giteaBaseURL, adminUsername, adminPassword, username string, pur req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return false, err + return false, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { - body, _ := io.ReadAll(resp.Body) - log.Println("Failed to delete user:", string(body)) - return false, nil + body, _ := io.ReadAll(resp.Body) //HS: What happens if this fails? + err := errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Failed to delete user: %s", string(body))) + log.Println(err.Error()) + return false, err } return true, nil } @@ -1106,19 +1048,19 @@ func handleDeleteUser(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.ErrRequestParseError) return } var options DeleteUserOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.ErrRequestParseError) return } if options.Username == "" { - http.Error(w, "Username must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Username must be provided")) return } @@ -1128,43 +1070,41 @@ func handleDeleteUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("User deleted successfully")) } else { - http.Error(w, "User deletion failed", http.StatusBadRequest) - if err != nil { - log.Printf("User deletion failed %v", err) - } else { - log.Printf("User deletion failed") + if err == nil { + err = errorapi.WrapError(errorapi.ErrInternalServerError, "User deletion failed") } + errorapi.HandleError(w, err) } } -func getUser(giteaBaseURL, adminUsername, adminPassword, username string) ([]byte, error) { +func getUser(giteaBaseURL, adminUsername, adminPassword, username string) ([]byte, *errorapi.APIError) { url := fmt.Sprintf("%s/users/%s", giteaBaseURL, username) req, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } // Set Basic Authentication header req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("error querying gitea: %v", err) + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("gitea returned status: %d", resp.StatusCode) - } - - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("error reading gitea response: %v", err) + var errapi *errorapi.APIError + if resp.StatusCode == 404 { + errapi = errorapi.WrapError(errorapi.ErrNotFound, fmt.Sprintf("did not find the user: %s", username)) + } else { + errapi = errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error getting user, gitea returned status: %d", resp.StatusCode)) + } + return nil, errapi } - + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrResponseReadError, err.Error()) } return bodyBytes, nil @@ -1174,7 +1114,7 @@ func handleGetUser(w http.ResponseWriter, r *http.Request) { // Retrieve the username from the query parameters username := r.URL.Query().Get("username") if username == "" { - http.Error(w, "Username not provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Username not provided")) return } @@ -1182,6 +1122,7 @@ func handleGetUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(resp) } else { + errorapi.HandleError(w, err) w.WriteHeader(http.StatusInternalServerError) } } @@ -1195,35 +1136,34 @@ func handleUser(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: handleDeleteUser(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func getSSHKeysForUser(giteaBaseURL, adminUsername, adminPassword, username string) ([]api.PublicKey, error) { +func getSSHKeysForUser(giteaBaseURL, adminUsername, adminPassword, username string) ([]api.PublicKey, *errorapi.APIError) { req, err := http.NewRequest("GET", giteaBaseURL+"/users/"+username+"/keys/", nil) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrResponseReadError, err.Error()) } var publicKeys []api.PublicKey err = json.Unmarshal(bodyBytes, &publicKeys) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrResponseReadError, fmt.Sprintf("Gitea response read error while getting user ssh keys: %v", err)) } return publicKeys, nil @@ -1232,7 +1172,7 @@ func getSSHKeysForUser(giteaBaseURL, adminUsername, adminPassword, username stri func handleGetUserSSHKeys(w http.ResponseWriter, r *http.Request) { username := r.URL.Query().Get("username") if username == "" { - http.Error(w, "Username must be provided to list SSH keys", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Username must be provided to list SSH keys")) return } @@ -1241,15 +1181,15 @@ func handleGetUserSSHKeys(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(jsonData)) } else { - http.Error(w, "Failed to list keys", http.StatusBadRequest) - log.Printf("Failed to list keys %v", err) + errormsg := fmt.Sprintf("Failed to get user ssh keys with error: %v.", err) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, errormsg)) } } -func deleteSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, name string) error { - keys, err := getSSHKeysForUser(giteaBaseURL, adminUsername, adminPassword, username) - if err != nil { - return err +func deleteSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, name string) *errorapi.APIError { + keys, reterr := getSSHKeysForUser(giteaBaseURL, adminUsername, adminPassword, username) + if reterr != nil { + return errorapi.WrapError(reterr, "Failed to get User SSH Keys") } var id int64 = -1 @@ -1260,25 +1200,25 @@ func deleteSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, n } } if id == -1 { - return nil + return nil // No key found } url := fmt.Sprintf("%s/admin/users/%s/keys/%d", giteaBaseURL, username, id) req, err := http.NewRequest("DELETE", url, nil) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Error deleting ssh keys: %v", err)) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Error sending delete request %v", err.Error())) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent && resp.StatusCode != 404 { - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Delete Keys request returned status: %d", resp.StatusCode)) } return nil @@ -1288,7 +1228,7 @@ func handleDeleteUserSSHKey(w http.ResponseWriter, r *http.Request) { keyName := r.URL.Query().Get("key_name") username := r.URL.Query().Get("username") if keyName == "" || username == "" { - http.Error(w, "Key name and username must be provided to delete the SSH key", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Key name and Username must be provided to delete SSH keys")) return } @@ -1296,15 +1236,14 @@ func handleDeleteUserSSHKey(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("Successfully deleted SSH key")) } else { - http.Error(w, "Key deletion failed", http.StatusBadRequest) - log.Printf("Key deletion failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(err, "Error Deleting SSH Key Pair")) } } -func createSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, key, name string) error { - err := deleteSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, name) - if err != nil { - return err +func createSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, key, name string) *errorapi.APIError { + reterr := deleteSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, name) + if reterr != nil { + return errorapi.WrapError(reterr, "Failed to delete existing User SSH Keys") } data := api.CreateKeyOption{ @@ -1317,32 +1256,30 @@ func createSSHKeyForUser(giteaBaseURL, adminUsername, adminPassword, username, k req, err := http.NewRequest("POST", giteaBaseURL+"/admin/users/"+username+"/keys", bytes.NewBuffer(jsonData)) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating ssh keys, returned error code: %d", resp.StatusCode)) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return err + return errorapi.WrapError(errorapi.ErrResponseReadError, err.Error()) } var publicKeyResponse api.PublicKey err = json.Unmarshal(bodyBytes, &publicKeyResponse) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return err + return errorapi.WrapError(errorapi.ErrResponseReadError, fmt.Sprintf("Gitea response read error after creating user ssh keys: %v", err)) } fmt.Println(publicKeyResponse.ID, publicKeyResponse.Key) @@ -1355,19 +1292,19 @@ func handleCreateUserSSHKey(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.ErrRequestReadError) return } var options CreateSSHOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.ErrRequestParseError) return } if options.Key == "" || options.KeyName == "" || options.Username == "" { - http.Error(w, "Key, KeyName, and Username must be provided to create the SSH key", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Key, KeyName, and Username must be provided to create the SSH key")) return } @@ -1375,8 +1312,7 @@ func handleCreateUserSSHKey(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("Successfully created SSH key")) } else { - http.Error(w, "Key creation failed", http.StatusBadRequest) - log.Printf("Key creation failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(err, "SSH Key creation failed")) } } @@ -1389,11 +1325,11 @@ func handleUserSsh(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: handleDeleteUserSSHKey(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, name, description string, private bool) (*api.Repository, error) { +func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, name, description string, private bool) (*api.Repository, *errorapi.APIError) { data := api.CreateRepoOption{ Name: name, Description: description, @@ -1404,19 +1340,19 @@ func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, nam req, err := http.NewRequest("POST", giteaBaseURL+"/admin/users/"+username+"/repos", bytes.NewBuffer(jsonData)) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Failed to create repo for user: %s, returned error code: %d", username, resp.StatusCode)) } var repository api.Repository @@ -1430,85 +1366,71 @@ func handleCreateRepo(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.ErrRequestReadError) return } var options RepoOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.ErrRequestParseError) return } if options.Name == "" || options.Description == "" || options.Owner == "" { - http.Error(w, "Name, description, and owner must be provided for the repo", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Name, description, and owner must be provided for the repo")) return } fmt.Println("Received Repo Data:", options) if repository, err := createRepoForUser(access.URL, access.Username, access.Password, options.Owner, options.Name, options.Description, options.Private); err == nil { - // if err := createWebhook(access.URL, access.Username, access.Password, options.Owner, options.Name, fullname); err == nil { - // remoteUrl := getRemoteUrlFromRepo(repository) - // w.WriteHeader(http.StatusCreated) - // w.Write([]byte(remoteUrl)) - // } else { - // http.Error(w, "Webhook creation failed", http.StatusBadRequest) - // log.Printf("Webhook creation failed %v", err) - // } remoteUrl := getRemoteUrlFromRepo(repository) w.WriteHeader(http.StatusCreated) w.Write([]byte(remoteUrl)) } else { - http.Error(w, "Repo creation failed", http.StatusBadRequest) - log.Printf("Repo creation failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Repo creation failed: %v", err))) } } -func listReposForUser(giteaBaseURL, adminUsername, adminPassword, owner string) ([]api.Repository, error) { +func listReposForUser(giteaBaseURL, adminUsername, adminPassword, owner string) ([]api.Repository, *errorapi.APIError) { // Build the Gitea API URL for fetching the repo details url := fmt.Sprintf("%s/users/%s/repos", giteaBaseURL, owner) // Create a new request req, err := http.NewRequest("GET", url, nil) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() // Check if the request was successful if resp.StatusCode != http.StatusOK { - log.Printf("Error fetching repos from Gitea %v", resp.StatusCode) - return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Status returned was not OK: %d", resp.StatusCode)) } // Read the response body from Gitea into a byte slice bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Error reading Gitea response %v", err)) } var repoList []api.Repository err = json.Unmarshal(bodyBytes, &repoList) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error()) } return repoList, nil } -func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string) (*api.Repository, error) { +func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string) (*api.Repository, *errorapi.APIError) { // Build the Gitea API URL for fetching the repo details url := fmt.Sprintf("%s/repos/%s/%s", giteaBaseURL, owner, repoName) @@ -1516,84 +1438,77 @@ func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName // Create a new request req, err := http.NewRequest("GET", url, nil) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() // Check if the request was successful if resp.StatusCode != http.StatusOK { - log.Printf("Error fetching repo from Gitea %v", resp.StatusCode) - return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("get user repo returned unexpected status: %d", resp.StatusCode)) } // Read the response body from Gitea into a byte slice bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrResponseReadError, err.Error()) } var repository api.Repository err = json.Unmarshal(bodyBytes, &repository) if err != nil { log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } return &repository, nil } -func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, treeishId, path string) ([]byte, error) { +func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, treeishId, path string) ([]byte, *errorapi.APIError) { // Build the Gitea API URL for downloading the repo archive url := fmt.Sprintf("%s/repos/%s/%s/archive/%s.zip", giteaBaseURL, owner, repoName, treeishId) // Build request req, err := http.NewRequest("GET", url, nil) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) // Send request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return nil, fmt.Errorf("HTTP Error: %v", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - log.Printf("Error downloading repo from Gitea %v %v", resp.StatusCode, url) - return nil, fmt.Errorf("HTTP Error: %v", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("downloading user repo returned unexpected status: %d", resp.StatusCode)) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.ErrResponseReadError } // Gitea does not currently support the `path` option // https://github.com/go-gitea/gitea/issues/4478 - archiveBytes, err := downloadPathFromZip(bodyBytes, fmt.Sprintf("%v/%v", repoName, path)) - if err != nil { + archiveBytes, apierr := downloadPathFromZip(bodyBytes, fmt.Sprintf("%v/%v", repoName, path)) + if apierr != nil { log.Printf("Error extracting path from zipfile %v", err) - return nil, err + return nil, apierr } - return archiveBytes, err + return archiveBytes, apierr } -func modifyRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string, newName *string, newDescription *string, newPrivate *bool) (*api.Repository, error) { +func modifyRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string, newName *string, newDescription *string, newPrivate *bool) (*api.Repository, *errorapi.APIError) { data := api.EditRepoOption{ Name: newName, Description: newDescription, @@ -1605,19 +1520,19 @@ func modifyRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoNa url := fmt.Sprintf("%s/repos/%s/%s", giteaBaseURL, owner, repoName) req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(jsonData)) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("modifying user repo returned unexpected status: %d", resp.StatusCode)) } var repository api.Repository @@ -1630,7 +1545,7 @@ func handlePatchRepo(w http.ResponseWriter, r *http.Request) { repoName := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") if repoName == "" || owner == "" { - http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Repo name and owner must be provided")) return } @@ -1638,14 +1553,14 @@ func handlePatchRepo(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.ErrRequestReadError) return } var options PatchRepoOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.ErrRequestParseError) return } @@ -1655,8 +1570,7 @@ func handlePatchRepo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(remoteUrl)) } else { - http.Error(w, "Repo modify failed", http.StatusBadRequest) - log.Printf("Repo modify failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(err, "Repo modify failed")) } } @@ -1664,7 +1578,7 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { repoName := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") if owner == "" { - http.Error(w, "Owner must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Owner must be provided")) return } if repoName == "" { @@ -1673,7 +1587,7 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(jsonData) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "repo list generation failed")) } } else { if repo, err := getRepoForUser(access.URL, access.Username, access.Password, owner, repoName); err == nil { @@ -1681,57 +1595,83 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(jsonData) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "Error getting repo for user")) } } } -func getRepoFileSHA(giteaBaseURL, adminUsername, adminPassword, owner, repoName, path, ref string) (string, error) { +func getRepoFile(giteaBaseURL, adminUsername, adminPassword, owner, repoName, path, ref string) ([]api.ContentsResponse, error) { url := fmt.Sprintf("%s/repos/%s/%s/contents/%s?ref=%s", giteaBaseURL, owner, repoName, path, ref) req, err := http.NewRequest("GET", url, nil) if err != nil { - return "", err + return nil, err } req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return "", err + return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Printf("Error reading Gitea response %v", err) - return "", err + return nil, err } - var contentsResponse api.ContentsResponse + + var contentsResponse []api.ContentsResponse err = json.Unmarshal(bodyBytes, &contentsResponse) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return "", err + var single api.ContentsResponse + err = json.Unmarshal(bodyBytes, &single) + if err == nil { + contentsResponse = []api.ContentsResponse{single} + } else { + log.Printf("Error reading Gitea response %v", err) + return nil, err + } } - return contentsResponse.SHA, nil + return contentsResponse, nil } -func modifyRepoFilesForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, branch, message string, files []*api.ChangeFileOperation) (string, error) { +func modifyRepoFilesForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, branch, message string, files []*api.ChangeFileOperation) (string, *errorapi.APIError) { // Build the Gitea API URL for downloading the repo archive url := fmt.Sprintf("%s/repos/%s/%s/contents", giteaBaseURL, owner, repoName) + var actualFiles []*api.ChangeFileOperation + for _, file := range files { - if file.Operation != "create" { - sha, err := getRepoFileSHA(giteaBaseURL, adminUsername, adminPassword, owner, repoName, file.Path, "") + if file.Operation == "create" { + actualFiles = append(actualFiles, file) + } else { + // Multiple files may be returned in the case of deleting a directory path + repoFiles, err := getRepoFile(giteaBaseURL, adminUsername, adminPassword, owner, repoName, file.Path, "") if err != nil { - log.Printf("Error getting SHA of '%v' from Gitea %v", file.Path, err) - return "", err + return "", errorapi.WrapError(errorapi.ErrInternalServerError, fmt.Sprintf("Error getting SHA of '%v' from Gitea %v", file.Path, err)) + } + if len(repoFiles) > 1 { + if file.Operation != "delete" { + log.Printf("Multiple files returned for path %s, cannot update a directory directly", file.Path) + return "", errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Cannot update directory %s directly", file.Path)) + } + for _, repoFile := range repoFiles { + actualFiles = append(actualFiles, &api.ChangeFileOperation{ + Operation: file.Operation, + Path: repoFile.Path, + SHA: repoFile.SHA, + }) + } + } else { + file.SHA = repoFiles[0].SHA + actualFiles = append(actualFiles, file) } - file.SHA = sha } } @@ -1742,16 +1682,16 @@ func modifyRepoFilesForUser(giteaBaseURL, adminUsername, adminPassword, owner, r Committer: api.Identity{Name: adminUsername}, Message: message, }, - Files: files, + Files: actualFiles, } - jsonData, _ := json.Marshal(data) + jsonData, _ := json.MarshalIndent(data, "", " ") + log.Printf("%s", jsonData) // Build request req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return "", err + return "", errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) @@ -1759,26 +1699,22 @@ func modifyRepoFilesForUser(giteaBaseURL, adminUsername, adminPassword, owner, r // Send request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return "", fmt.Errorf("HTTP Error: %v", resp.StatusCode) + return "", errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - log.Printf("Error modifying repo from Gitea %v %v", resp.StatusCode, url) - return "", fmt.Errorf("HTTP Error: %v", resp.StatusCode) + return "", errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Error modifying repo from Gitea %v %v", resp.StatusCode, url)) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return "", err + return "", errorapi.WrapError(errorapi.ErrRequestReadError, err.Error()) } var filesResponse api.FilesResponse err = json.Unmarshal(bodyBytes, &filesResponse) if err != nil { - log.Printf("Error reading Gitea response %v", err) - return "", err + return "", errorapi.WrapError(errorapi.ErrRequestParseError, err.Error()) } return filesResponse.Commit.SHA, nil @@ -1789,14 +1725,14 @@ func handleModifyRepoFiles(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, err.Error())) return } var options ModifyRepoOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } @@ -1804,8 +1740,7 @@ func handleModifyRepoFiles(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(commitHash)) } else { - http.Error(w, "Repo modify failed", http.StatusBadRequest) - log.Printf("Repo modify failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(err, "Modify repo failed")) } } @@ -1816,18 +1751,18 @@ func handleDownloadRepo(w http.ResponseWriter, r *http.Request) { path := r.URL.Query().Get("path") if repoName == "" || owner == "" { - http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Repo name and owner must be provided")) return } if resp, err := downloadRepoForUser(access.URL, access.Username, access.Password, owner, repoName, treeishId, path); err == nil { w.WriteHeader(http.StatusOK) w.Write(resp) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, err) } } -func listCommitsForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, branch string) ([]api.Commit, error) { +func listCommitsForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, branch string) ([]api.Commit, *errorapi.APIError) { // Build the Gitea API URL for downloading the repo archive url := fmt.Sprintf("%s/repos/%s/%s/commits?sha=%s&files=false", giteaBaseURL, owner, repoName, branch) @@ -1835,7 +1770,7 @@ func listCommitsForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoN req, err := http.NewRequest("GET", url, nil) if err != nil { log.Printf("Error creating request %v", http.StatusInternalServerError) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) @@ -1843,26 +1778,26 @@ func listCommitsForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoN resp, err := http.DefaultClient.Do(req) if err != nil { log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return nil, fmt.Errorf("HTTP Error: %v", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("Error listing commits from Gitea %v", resp.StatusCode) - return nil, fmt.Errorf("HTTP Error: %v", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("listing commits failed with status code: %d", resp.StatusCode)) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrRequestReadError, err.Error()) } var commits []api.Commit err = json.Unmarshal(bodyBytes, &commits) if err != nil { log.Printf("Error reading Gitea response %v", err) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error()) } return commits, nil @@ -1882,11 +1817,11 @@ func handleListCommits(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(jsonData) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "failed to list commits for repo")) } } -func deleteRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string) error { +func deleteRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string) *errorapi.APIError { // Build the Gitea API URL for fetching the repo details url := fmt.Sprintf("%s/repos/%s/%s", giteaBaseURL, owner, repoName) @@ -1894,22 +1829,20 @@ func deleteRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoNa // Create a new request req, err := http.NewRequest("DELETE", url, nil) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { log.Printf("Error deleting repo from Gitea %v", resp.StatusCode) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("repo deletion failed with status code: %d", resp.StatusCode)) } return nil @@ -1920,14 +1853,14 @@ func handleDeleteRepo(w http.ResponseWriter, r *http.Request) { owner := r.URL.Query().Get("owner") if repoName == "" || owner == "" { - http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Repo name and owner must be provided")) return } if err := deleteRepoForUser(access.URL, access.Username, access.Password, owner, repoName); err == nil { w.WriteHeader(http.StatusOK) w.Write([]byte("Successfully deleted repository")) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "repo deletion failed")) } } @@ -1942,11 +1875,11 @@ func handleRepo(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: handleDeleteRepo(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func addCollaboratorToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, collaboratorName, permission string) error { +func addCollaboratorToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, collaboratorName, permission string) *errorapi.APIError { // Build the Gitea API URL for fetching the repo details url := fmt.Sprintf("%s/repos/%s/%s/collaborators/%s", giteaBaseURL, owner, repoName, collaboratorName) @@ -1960,8 +1893,7 @@ func addCollaboratorToRepo(giteaBaseURL, adminUsername, adminPassword, owner, re // Create a new request req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData)) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) @@ -1970,14 +1902,13 @@ func addCollaboratorToRepo(giteaBaseURL, adminUsername, adminPassword, owner, re resp, err := http.DefaultClient.Do(req) if err != nil { log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() // Check if the request was successful if resp.StatusCode != http.StatusNoContent { - log.Printf("Error adding contributor from Gitea %v", resp.StatusCode) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Error adding contributor from Gitea %d", resp.StatusCode)) } return nil @@ -1988,31 +1919,31 @@ func handleAddCollaborator(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, err.Error())) return } var options AddCollaboratorOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } // We won't enforce Permission since Gitea doesn't enforce it. if options.Name == "" || options.Owner == "" || options.CollaboratorName == "" { - http.Error(w, "Repo name, owner, and collaborator name must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Repo name, owner, and collaborator name must be provided")) return } if err := addCollaboratorToRepo(access.URL, access.Username, access.Password, options.Owner, options.Name, options.CollaboratorName, options.Permission); err == nil { w.WriteHeader(http.StatusOK) w.Write([]byte("Contributor added successfully")) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, err) } } -func removeCollaboratorFromRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, collaboratorName string) error { +func removeCollaboratorFromRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, collaboratorName string) *errorapi.APIError { // Build the Gitea API URL for fetching the repo details url := fmt.Sprintf("%s/repos/%s/%s/collaborators/%s", giteaBaseURL, owner, repoName, collaboratorName) @@ -2020,8 +1951,7 @@ func removeCollaboratorFromRepo(giteaBaseURL, adminUsername, adminPassword, owne // Create a new request req, err := http.NewRequest("DELETE", url, nil) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) @@ -2029,15 +1959,13 @@ func removeCollaboratorFromRepo(giteaBaseURL, adminUsername, adminPassword, owne // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() // Check if the request was successful if resp.StatusCode != http.StatusNoContent { - log.Printf("Error removing contributor from Gitea %v", resp.StatusCode) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Removing collaborator returned error code: %d", resp.StatusCode)) } return nil @@ -2048,26 +1976,26 @@ func handleRemoveCollaborator(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, err.Error())) return } var options RemoveCollaboratorOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } if options.Name == "" || options.Owner == "" || options.CollaboratorName == "" { - http.Error(w, "Repo name, owner, and collaborator name must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Repo name, owner, and collaborator name must be provided")) return } if err := removeCollaboratorFromRepo(access.URL, access.Username, access.Password, options.Owner, options.Name, options.CollaboratorName); err == nil { w.WriteHeader(http.StatusOK) w.Write([]byte("Contributor removed successfully")) } else { - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "")) } } @@ -2078,11 +2006,11 @@ func handleRepoCollaborator(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: handleRemoveCollaborator(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func addHookToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, hookId, content string) error { +func addHookToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, hookId, content string) *errorapi.APIError { // Build the Gitea API URL for fetching the repo details url := fmt.Sprintf("%s/repos/%s/%s/hooks/git/%s", giteaBaseURL, owner, repoName, hookId) @@ -2096,8 +2024,7 @@ func addHookToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(jsonData)) log.Println(url) if err != nil { - log.Printf("Error creating request %v", http.StatusInternalServerError) - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) @@ -2105,15 +2032,13 @@ func addHookToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error querying Gitea %v", http.StatusInternalServerError) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() // Check if the request was successful if resp.StatusCode != http.StatusOK { - log.Printf("Error adding Git hook from Gitea %v", resp.StatusCode) - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Error adding Git hook from Gitea %v", resp.StatusCode)) } return nil @@ -2123,32 +2048,32 @@ func handleAddHook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestReadError, err.Error())) return } var options AddHookOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestReadError, err.Error())) return } if options.Name == "" || options.Owner == "" || options.HookId == "" { - http.Error(w, "Repo name, owner, and hook id must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Repo name, owner, and hook id must be provided")) return } if err := addHookToRepo(access.URL, access.Username, access.Password, options.Owner, options.Name, options.HookId, options.Content); err == nil { w.WriteHeader(http.StatusOK) w.Write([]byte("Hook added successfully")) } else { - http.Error(w, "Failed to add hook", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, err.Error())) } } @@ -2157,11 +2082,11 @@ func handleRepoHook(w http.ResponseWriter, r *http.Request) { case http.MethodPut: handleAddHook(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) (*api.Repository, error) { +func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) (*api.Repository, *errorapi.APIError) { /* reenable this once gitea bug #26234 is fixed @@ -2180,7 +2105,7 @@ func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, re req, err := http.NewRequest("POST", giteaBaseURL+"/repos/"+owner+"/"+repo+"/forks", bytes.NewBuffer(jsonData)) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.Header.Add("Content-Type", "application/json") @@ -2188,34 +2113,30 @@ func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, re resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusAccepted { if err := transferRepoOwnership(giteaBaseURL, adminUsername, adminPassword, adminUsername, tmpRepoName, user); err != nil { log.Printf("transfer ownership of %s to %s failed: %v", tmpRepoName, user, err) - // We failed to transfer, so fork still on admin account - deleteRepoForUser(giteaBaseURL, adminUsername, adminPassword, adminUsername, tmpRepoName) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } if err := renameRepo(giteaBaseURL, adminUsername, adminPassword, user, tmpRepoName, repo); err != nil { log.Printf("rename of repo from %s to %s failed %v", tmpRepoName, repo, err) - // Transferred but failed to rename - deleteRepoForUser(giteaBaseURL, adminUsername, adminPassword, user, tmpRepoName) - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) + } + if err := createWebhook(access.URL, access.Username, access.Password, user, repo, fullname); err != nil { + log.Printf("create webhook for repo %s failed %v", repo, err) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } - // if err := createWebhook(access.URL, access.Username, access.Password, user, repo, fullname); err != nil { - // log.Printf("create webhook for repo %s failed %v", repo, err) - // return nil, err - // } var repository api.Repository json.NewDecoder(resp.Body).Decode(&repository) return &repository, nil } else { - return nil, fmt.Errorf("fork failed with code %v", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("fork failed with code %v", resp.StatusCode)) } } @@ -2223,14 +2144,14 @@ func handleCreateFork(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, err.Error())) return } var options ForkOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } @@ -2241,17 +2162,15 @@ func handleCreateFork(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte(remoteUrl)) } else { - http.Error(w, "Fork failed", http.StatusBadRequest) - log.Printf("Repo creation failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(err, "Repo Creation Failed")) deleteRepoForUser(access.URL, access.Username, access.Password, options.NewOwner, options.Repo) } } else { - http.Error(w, "Fork failed", http.StatusBadRequest) + errormsg := "Repo creation failed: " if err != nil { - log.Printf("Repo creation failed %v", err) - } else { - log.Printf("Repo creation failed") + errormsg += err.Error() } + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, errormsg)) } } @@ -2259,7 +2178,7 @@ func handleGetForks(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") if name == "" || owner == "" { - http.Error(w, "Fork name and owner must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Fork name and owner must be provided")) return } @@ -2269,12 +2188,10 @@ func handleGetForks(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(bytes) } else { - log.Printf("Unable to parse findForks result %v", err) - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, err.Error())) } } else { - log.Printf("findForks failed %v", err) - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, err) } } @@ -2285,27 +2202,27 @@ func handleFork(w http.ResponseWriter, r *http.Request) { case http.MethodGet: handleGetForks(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func getOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) (*api.Organization, error) { +func getOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) (*api.Organization, *errorapi.APIError) { req, err := http.NewRequest("GET", giteaBaseURL+"/orgs/"+orgName, nil) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to get organization details; HTTP status code: %d", resp.StatusCode) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to get organization details; HTTP status code: %d", resp.StatusCode)) } var orgDetails api.Organization @@ -2318,7 +2235,7 @@ func getOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) (*api.Or func handleGetOrg(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("org_name") if name == "" { - http.Error(w, "Orgname be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Org Name must be provided")) return } @@ -2327,16 +2244,14 @@ func handleGetOrg(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(bytes) } else { - log.Printf("Unable to parse getOrg result %v", err) - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) } } else { - log.Printf("getOrg failed %v", err) - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, err) } } -func createOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) error { +func createOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) *errorapi.APIError { options := api.CreateOrgOption{ UserName: orgName, Visibility: "public", @@ -2346,7 +2261,7 @@ func createOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) error req, err := http.NewRequest("POST", giteaBaseURL+"/orgs", bytes.NewBuffer(jsonData)) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) @@ -2354,12 +2269,12 @@ func createOrg(giteaBaseURL, adminUsername, adminPassword, orgName string) error resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("failed to create organization; HTTP status code: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to create organization; HTTP status code: %d", resp.StatusCode)) } return nil @@ -2370,19 +2285,19 @@ func handleCreateOrg(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err != nil { - http.Error(w, "Failed reading request body", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestReadError, err.Error())) return } var options OrgOptions err = json.Unmarshal(body, &options) if err != nil { - http.Error(w, "Failed parsing request body", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrRequestParseError, err.Error())) return } if options.OrgName == "" { - http.Error(w, "name must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "orgname must be provided")) return } @@ -2392,21 +2307,18 @@ func handleCreateOrg(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("Org created successfully")) } else { - http.Error(w, "Org-Team creation failed", http.StatusBadRequest) - log.Printf("Org-Team creation failed %v", err) - deleteOrg(access.URL, access.Username, access.Password, options.OrgName, true) + errorapi.HandleError(w, errorapi.WrapError(err, fmt.Sprintf("Org-Team creation failed %v", err))) } } else { - http.Error(w, "Org creation failed", http.StatusBadRequest) - log.Printf("Org creation failed %v", err) + errorapi.HandleError(w, errorapi.WrapError(err, fmt.Sprintf("Org creation failed %v", err))) } } -func deleteOrg(giteaBaseURL, adminUsername, adminPassword, orgName string, purge bool) error { +func deleteOrg(giteaBaseURL, adminUsername, adminPassword, orgName string, purge bool) *errorapi.APIError { if purge { repos, err := listReposForUser(giteaBaseURL, adminUsername, adminPassword, orgName) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } for _, repository := range repos { deleteRepoForUser(giteaBaseURL, adminUsername, adminPassword, orgName, repository.Name) @@ -2415,19 +2327,19 @@ func deleteOrg(giteaBaseURL, adminUsername, adminPassword, orgName string, purge req, err := http.NewRequest("DELETE", giteaBaseURL+"/orgs/"+orgName, nil) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { - return fmt.Errorf("failed to delete organization; HTTP status code: %d", resp.StatusCode) + return errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to delete organization; HTTP status code: %d", resp.StatusCode)) } return nil @@ -2438,7 +2350,7 @@ func handleDeleteOrg(w http.ResponseWriter, r *http.Request) { purge := r.URL.Query().Get("purge") == "true" if name == "" { - http.Error(w, "org_name must be provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Org name must be provided as org_name")) return } @@ -2448,6 +2360,7 @@ func handleDeleteOrg(w http.ResponseWriter, r *http.Request) { } else { log.Printf("failed to delete org %v", err) w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "failed to delete org")) } } @@ -2460,34 +2373,34 @@ func handleOrg(w http.ResponseWriter, r *http.Request) { case http.MethodGet: handleGetOrg(w, r) default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrMethodNotAllowed, r.Method)) } } -func getOrgMembers(giteaBaseURL, adminUsername, adminPassword, orgName string) ([]api.User, error) { +func getOrgMembers(giteaBaseURL, adminUsername, adminPassword, orgName string) ([]api.User, *errorapi.APIError) { teamID, err := getTeamID(giteaBaseURL, adminUsername, adminPassword, orgName, DEFAULT_TEAM_NAME) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, err.Error()) } reqURL := fmt.Sprintf("%s/teams/%d/members", giteaBaseURL, teamID) req, err := http.NewRequest("GET", reqURL, nil) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("error creating request: %v", err)) } req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return nil, errorapi.WrapError(errorapi.ErrGiteaConnectError, err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { var responseError map[string]interface{} json.NewDecoder(resp.Body).Decode(&responseError) - return nil, fmt.Errorf("failed to get team members; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + return nil, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("failed to get team members; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"])) } var members []api.User @@ -2501,7 +2414,7 @@ func handleGetMembers(w http.ResponseWriter, r *http.Request) { orgName := vars["orgName"] if orgName == "" { - http.Error(w, "Orgname not provided", http.StatusBadRequest) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, "Orgname not provided")) return } @@ -2510,12 +2423,10 @@ func handleGetMembers(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(bytes) } else { - log.Printf("Unable to parse getMembers result %v", err) - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(errorapi.ErrBadRequest, fmt.Sprintf("Unable to parse getMembers result %v", err))) } } else { - log.Printf("getMembers failed %v", err) - w.WriteHeader(http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "GetMembers Failed")) } } @@ -2529,8 +2440,7 @@ func handleAddMember(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("User added to organization")) } else { - log.Printf("%v", err) - http.Error(w, "Add user failed", http.StatusInternalServerError) + errorapi.HandleError(w, errorapi.WrapError(err, "Add User Failed")) } } @@ -2558,9 +2468,10 @@ func livenessHandler(w http.ResponseWriter, r *http.Request) { // main initializes an HTTP server with endpoints for processing push events, // checking service readiness, and determining service liveness. The server -// listens on port 8900. Logging is utilized to indicate the server's start +// listens on port 9000. Logging is utilized to indicate the server's start // and to capture any fatal errors. func main() { +<<<<<<< HEAD //mux := http.NewServeMux() r := mux.NewRouter() r.HandleFunc("/onPush", webhookHandler) @@ -2579,6 +2490,48 @@ func main() { r.HandleFunc("/readiness", readinessHandler) r.HandleFunc("/liveness", livenessHandler) http.Handle("/", r) +======= + router := mux.NewRouter() + // All protected routes must use AuthMiddleware + protected := router.PathPrefix("/").Subrouter() + protected.Use(AuthMiddleware) + + protected.HandleFunc("/users", handleUser) + protected.HandleFunc("/users/ssh", handleUserSsh) + protected.HandleFunc("/repos", handleRepo) + protected.HandleFunc("/repos/collaborators", handleRepoCollaborator) + protected.HandleFunc("/repos/hooks", handleRepoHook) + protected.HandleFunc("/repos/modify", handleModifyRepoFiles).Methods("POST") + protected.HandleFunc("/repos/download", handleDownloadRepo).Methods("GET") + protected.HandleFunc("/forks", handleFork) + protected.HandleFunc("/orgs", handleOrg) + protected.HandleFunc("/orgs/{orgName}/members", handleGetMembers).Methods("GET") + protected.HandleFunc("/orgs/{orgName}/members/{userName}", handleAddMember).Methods("PUT") + // The router routes will not use the auth middleware + router.HandleFunc("/readiness", readinessHandler) + router.HandleFunc("/liveness", livenessHandler) + + srv := &http.Server{ + Handler: router, + Addr: "0.0.0.0:9000", + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + +>>>>>>> 166f0a2f3eb0850f234daff7858c8f346f1d256a log.Println("Server started on :9000") - log.Fatal(http.ListenAndServe(":9000", nil)) + go func() { + err := srv.ListenAndServe() + log.Fatalf(err.Error()) + }() + + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-shutdown + + //Shutdown gracefully + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + log.Print("Received shutdown signal closing server ...") + srv.Shutdown(ctx) } diff --git a/mk_passwd.py b/mk_passwd.py index e60565a..9d3f6cc 100755 --- a/mk_passwd.py +++ b/mk_passwd.py @@ -6,9 +6,10 @@ import argparse # Argument parser setup -parser = argparse.ArgumentParser(description="Create a Kubernetes secret with a random or predefined password and username.") +parser = argparse.ArgumentParser(description="Create a Kubernetes secret with a random or predefined gitea password, username and gitea-assist auth token.") parser.add_argument('--password', type=str, help='Predefined password to use. If not provided, a random password will be generated.') parser.add_argument('--username', type=str, default='gitea_admin', help='Username to use. Defaults to "gitea_admin".') +parser.add_argument('--token', type=str, help='Gitea-Assist token for Authentication Bearer [token] exchange with the API itself.') args = parser.parse_args() @@ -16,6 +17,7 @@ alphabet = string.ascii_letters + string.digits password = args.password if args.password else ''.join(secrets.choice(alphabet) for i in range(12)) username = args.username +token = args.token if args.token else ''.join(secrets.choice(alphabet) for i in range(15)) # Name of the secret secret_name = "gitea-assist-creds" @@ -33,7 +35,8 @@ "generic", secret_name, "--from-literal=gitea-password={}".format(password), - "--from-literal=gitea-username={}".format(username) + "--from-literal=gitea-username={}".format(username), + "--from-literal=assist-token={}".format(token) ] subprocess.run(create_secret_command, check=True) else: