diff --git a/README.md b/README.md index 1b1f5db..19f0cae 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ To run the application, simply execute: go run main.go ``` -This will start an HTTP server on port 8000. +This will start an HTTP server on port 9000. ## Kubernetes Secret Creator for Gitea Credentials (mk_passwd.py) diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index f590d77..e47bafe 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -40,11 +40,11 @@ spec: livenessProbe: httpGet: path: /liveness - port: 8000 + port: 9000 readinessProbe: httpGet: path: /readiness - port: 8000 + port: 9000 resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: diff --git a/chart/values.yaml b/chart/values.yaml index 8ed2469..bad236f 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -38,7 +38,7 @@ securityContext: {} service: type: ClusterIP - port: 8000 + port: 9000 ingress: enabled: false diff --git a/main.go b/main.go index b414ffe..6b68cba 100644 --- a/main.go +++ b/main.go @@ -46,12 +46,17 @@ type MergeContext struct { ForkIsEmpty bool } -type UserOptions struct { +type CreateUserOptions struct { Email string `json:"email"` Username string `json:"username"` Password string `json:"password"` } +type DeleteUserOptions struct { + Username string `json:"username"` + Purge bool `json:"purge"` +} + type RepoOptions struct { Name string `json:"name"` Description string `json:"description"` @@ -220,6 +225,40 @@ func findForks(repoURL, username, password string) ([]api.Repository, error) { return forks, nil } +func getRemoteUrlFromRepo(repo *api.Repository) string { + return repo.CloneURL +} + +func getRemoteUrl(giteaBaseURL, adminUsername, adminPassword, owner string, repo string) (string, error) { + 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 + } + + 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 + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + 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"]) + } + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return getRemoteUrlFromRepo(&repository), nil +} + func transferRepoOwnership(giteaBaseURL, adminUsername, adminPassword, owner, repo, newOwner string) error { options := api.TransferRepoOption{ NewOwner: newOwner, @@ -361,7 +400,7 @@ func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName 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"]) } @@ -369,12 +408,43 @@ func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName return nil } +func deleteUserFromTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, userName string) error { + teamID, err := getTeamID(giteaBaseURL, adminUsername, adminPassword, orgName, teamName) + if err != nil { + return err + } + + 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 + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + 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 delete user from team; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + } + + return nil +} + func createWebhook(giteaBaseURL, adminUsername, adminPassword, owner, repo, fullname string) error { reqURL := fmt.Sprintf("%s/repos/%s/%s/hooks", giteaBaseURL, owner, repo) config := api.CreateHookOptionConfig{ "content_type": "json", - "url": "http://" + fullname + ":8000/onPush", + "url": "http://" + fullname + ":9000/onPush", } options := api.CreateHookOption{ @@ -849,10 +919,13 @@ func createUser(giteaBaseURL, adminUsername, adminPassword, username, password, Password string `json:"password" binding:"Required;MaxSize(255)"` } */ + mustChangePassword := false user := api.CreateUserOption{ Username: username, Email: email, Password: password, + // I have no idea why this wants a pointer to a bool... + MustChangePassword: &mustChangePassword, } jsonData, _ := json.Marshal(user) @@ -884,7 +957,7 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) { return } - var options UserOptions + var options CreateUserOptions err = json.Unmarshal(body, &options) if err != nil { http.Error(w, "Failed parsing request body", http.StatusBadRequest) @@ -911,6 +984,62 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) { } } +func deleteUser(giteaBaseURL, adminUsername, adminPassword, username string, purge bool) (bool, error) { + url := fmt.Sprintf("%s/admin/users/%s?purge=%t", giteaBaseURL, username, purge) + req, _ := http.NewRequest("DELETE", url, nil) + + 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 + } + 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 + } + return true, nil +} + +func handleDeleteUser(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) + return + } + + var options DeleteUserOptions + err = json.Unmarshal(body, &options) + if err != nil { + http.Error(w, "Failed parsing request body", http.StatusBadRequest) + return + } + + if options.Username == "" { + http.Error(w, "Username must be provided", http.StatusBadRequest) + return + } + + log.Println("Received User Data:", options) + if success, err := deleteUser(access.URL, access.Username, access.Password, options.Username, options.Purge); success { + // Respond to the client + w.WriteHeader(http.StatusCreated) + 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") + } + } +} + func getUser(giteaBaseURL, adminUsername, adminPassword, username string) ([]byte, error) { url := fmt.Sprintf("%s/users/%s", giteaBaseURL, username) @@ -966,12 +1095,14 @@ func handleUser(w http.ResponseWriter, r *http.Request) { handleCreateUser(w, r) case http.MethodGet: handleGetUser(w, r) + case http.MethodDelete: + handleDeleteUser(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, name, description string, private bool) error { +func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, name, description string, private bool) (*api.Repository, error) { data := api.CreateRepoOption{ Name: name, Description: description, @@ -982,21 +1113,25 @@ func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, nam req, err := http.NewRequest("POST", giteaBaseURL+"/admin/users/"+username+"/repos", bytes.NewBuffer(jsonData)) if err != nil { - return err + return nil, 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 nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) } - return nil + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return &repository, nil } func handleCreateRepo(w http.ResponseWriter, r *http.Request) { @@ -1021,10 +1156,11 @@ func handleCreateRepo(w http.ResponseWriter, r *http.Request) { } fmt.Println("Received Repo Data:", options) - if err := createRepoForUser(access.URL, access.Username, access.Password, options.Owner, options.Name, options.Description, options.Private); err == nil { + 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("Repo created successfully")) + w.Write([]byte(remoteUrl)) } else { http.Error(w, "Webhook creation failed", http.StatusBadRequest) log.Printf("Webhook creation failed %v", err) @@ -1033,10 +1169,6 @@ func handleCreateRepo(w http.ResponseWriter, r *http.Request) { http.Error(w, "Repo creation failed", http.StatusBadRequest) log.Printf("Repo creation failed %v", err) } - - // Respond to the client - w.WriteHeader(http.StatusCreated) - w.Write([]byte("Repo created successfully")) } func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string) ([]byte, error) { @@ -1076,6 +1208,40 @@ func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName return bodyBytes, nil } +func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string, commit string) ([]byte, error) { + // Build the Gitea API URL for downloading the repo archive + url := fmt.Sprintf("%s/repos/%s/%s/archive/%s.zip", giteaBaseURL, owner, repoName, commit) + + // Build request + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("Error creating request %v", http.StatusInternalServerError) + return nil, 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) + } + 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) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Error reading Gitea response %v", err) + return nil, err + } + + return bodyBytes, nil +} + func handleGetRepo(w http.ResponseWriter, r *http.Request) { repoName := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") @@ -1091,6 +1257,23 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { } } +func handleDownloadRepo(w http.ResponseWriter, r *http.Request) { + repoName := r.URL.Query().Get("name") + owner := r.URL.Query().Get("owner") + commit := r.URL.Query().Get("commit") + if repoName == "" || owner == "" { + http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) + return + } + if resp, err := downloadRepoForUser(access.URL, access.Username, access.Password, owner, repoName, commit); err == nil { + w.WriteHeader(http.StatusOK) + w.Write(resp) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + +} + func handleRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: @@ -1102,7 +1285,7 @@ func handleRepo(w http.ResponseWriter, r *http.Request) { } } -func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) error { +func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) (*api.Repository, error) { /* reenable this once gitea bug #26234 is fixed @@ -1121,7 +1304,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 err + return nil, err } req.Header.Add("Content-Type", "application/json") @@ -1129,26 +1312,30 @@ func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, re resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return nil, err } 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) - return err + return nil, err } 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) - return err + return nil, err } 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 err + return nil, err } - return nil + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return &repository, nil } else { - return fmt.Errorf("fork failed with code %v", resp.StatusCode) + return nil, fmt.Errorf("fork failed with code %v", resp.StatusCode) } } @@ -1168,9 +1355,15 @@ func handleCreateFork(w http.ResponseWriter, r *http.Request) { } fmt.Println("Forking repo:", options.Repo, "for user:", options.NewOwner) - if err := forkRepositoryForUser(access.URL, access.Username, access.Password, options.Owner, options.Repo, options.NewOwner); err == nil { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(fmt.Sprintf("Repo %s forked successfully for user %s", options.Repo, options.NewOwner))) + if _, err := forkRepositoryForUser(access.URL, access.Username, access.Password, options.Owner, options.Repo, options.NewOwner); err == nil { + // Note: we can't use getRemoteUrlFromRepo since the returned repo remote is incorrect due to the way we handle forking w/ rename. + if remoteUrl, err := getRemoteUrl(access.URL, access.Username, access.Password, options.NewOwner, options.Repo); err == nil { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(remoteUrl)) + } else { + http.Error(w, "Fork failed", http.StatusBadRequest) + log.Printf("Repo creation failed %v", err) + } } else { http.Error(w, "Fork failed", http.StatusBadRequest) if err != nil { @@ -1403,6 +1596,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) } } @@ -1431,7 +1625,7 @@ 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 8000. Logging is utilized to indicate the server's start +// listens on port 8900. Logging is utilized to indicate the server's start // and to capture any fatal errors. func main() { //mux := http.NewServeMux() @@ -1439,6 +1633,7 @@ func main() { r.HandleFunc("/onPush", webhookHandler) r.HandleFunc("/users", handleUser) r.HandleFunc("/repos", handleRepo) + r.HandleFunc("/repos/download", handleDownloadRepo).Methods("GET") r.HandleFunc("/forks", handleFork) r.HandleFunc("/orgs", handleOrg) r.HandleFunc("/orgs/{orgName}/members", handleGetMembers).Methods("GET") @@ -1446,6 +1641,6 @@ func main() { r.HandleFunc("/readiness", readinessHandler) r.HandleFunc("/liveness", livenessHandler) http.Handle("/", r) - log.Println("Server started on :8000") - log.Fatal(http.ListenAndServe(":8000", nil)) + log.Println("Server started on :9000") + log.Fatal(http.ListenAndServe(":9000", nil)) } diff --git a/test_scripts/test_add_member.py b/test_scripts/test_add_member.py index cffb57e..2e5b0e5 100644 --- a/test_scripts/test_add_member.py +++ b/test_scripts/test_add_member.py @@ -11,7 +11,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs/{args.org_name}/members/{args.user_name}" + url = f"http://{args.server}:9000/orgs/{args.org_name}/members/{args.user_name}" response = requests.put(url) diff --git a/test_scripts/test_create_fork.py b/test_scripts/test_create_fork.py index 07f0cdd..ca3a7a5 100644 --- a/test_scripts/test_create_fork.py +++ b/test_scripts/test_create_fork.py @@ -13,7 +13,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/forks" + url = f"http://{args.server}:9000/forks" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_create_org.py b/test_scripts/test_create_org.py index a118833..13e39b1 100644 --- a/test_scripts/test_create_org.py +++ b/test_scripts/test_create_org.py @@ -11,7 +11,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs" + url = f"http://{args.server}:9000/orgs" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_create_repo.py b/test_scripts/test_create_repo.py index 40d5c78..e4383ee 100644 --- a/test_scripts/test_create_repo.py +++ b/test_scripts/test_create_repo.py @@ -12,7 +12,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/repos" + url = f"http://{args.server}:9000/repos" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_create_user.py b/test_scripts/test_create_user.py index 661140a..393b3ea 100644 --- a/test_scripts/test_create_user.py +++ b/test_scripts/test_create_user.py @@ -13,7 +13,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/users" + url = f"http://{args.server}:9000/users" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_delete_user.py b/test_scripts/test_delete_user.py new file mode 100644 index 0000000..b70221f --- /dev/null +++ b/test_scripts/test_delete_user.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import argparse +import requests +import json + +def main(): + parser = argparse.ArgumentParser(description="Test user deletion via API.") + parser.add_argument("username", help="Username of the user") + parser.add_argument("--purge", action="store_true", help="Completely purge the user from the system (repositories, membership, etc.)") + parser.add_argument("--server", default="localhost", help="Server hostname with port (default: localhost)") + + args = parser.parse_args() + + url = f"http://{args.server}:9000/users" + headers = { + "Content-Type": "application/json" + } + data = { + "username": args.username, + "purge": args.purge + } + + response = requests.delete(url, headers=headers, data=json.dumps(data)) + + print(response.status_code) + print(response.text) + +if __name__ == "__main__": + main() diff --git a/test_scripts/test_get_forks.py b/test_scripts/test_get_forks.py index 2838e36..ec0025b 100644 --- a/test_scripts/test_get_forks.py +++ b/test_scripts/test_get_forks.py @@ -11,7 +11,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/forks?name={args.repo_name}&owner={args.owner}" + url = f"http://{args.server}:9000/forks?name={args.repo_name}&owner={args.owner}" response = requests.get(url) diff --git a/test_scripts/test_get_org.py b/test_scripts/test_get_org.py index 27f59cc..7a25f7f 100644 --- a/test_scripts/test_get_org.py +++ b/test_scripts/test_get_org.py @@ -10,7 +10,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs?org_name={args.org_name}" + url = f"http://{args.server}:9000/orgs?org_name={args.org_name}" response = requests.get(url) diff --git a/test_scripts/test_get_org_members.py b/test_scripts/test_get_org_members.py index 52e6fa3..9e719f5 100644 --- a/test_scripts/test_get_org_members.py +++ b/test_scripts/test_get_org_members.py @@ -10,7 +10,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs/{args.org_name}/members" + url = f"http://{args.server}:9000/orgs/{args.org_name}/members" response = requests.get(url) diff --git a/test_scripts/test_get_repo.py b/test_scripts/test_get_repo.py index c85b6e8..a921986 100644 --- a/test_scripts/test_get_repo.py +++ b/test_scripts/test_get_repo.py @@ -7,11 +7,11 @@ def main(): parser = argparse.ArgumentParser(description="Test getting repo details via API.") parser.add_argument("repo_name", help="Name of the repository") parser.add_argument("owner", help="Owner of the repository") - parser.add_argument("--server", default="localhost:8000", help="Server hostname with port (default: localhost:8000)") + parser.add_argument("--server", default="localhost", help="Server hostname with port (default: localhost)") args = parser.parse_args() - url = f"http://{args.server}/repos?name={args.repo_name}&owner={args.owner}" + url = f"http://{args.server}:9000/repos?name={args.repo_name}&owner={args.owner}" response = requests.get(url) diff --git a/test_scripts/test_get_user.py b/test_scripts/test_get_user.py index 7e74133..19ebf9c 100644 --- a/test_scripts/test_get_user.py +++ b/test_scripts/test_get_user.py @@ -10,7 +10,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/users?username={args.username}" + url = f"http://{args.server}:9000/users?username={args.username}" response = requests.get(url)