build source
This commit is contained in:
commit
ee1fec43ed
4171 changed files with 1351288 additions and 0 deletions
264
internal/forgejo/client.go
Normal file
264
internal/forgejo/client.go
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
package forgejo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Client talks to the Forgejo admin API for user management.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
adminUser string
|
||||
adminPassword string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a Forgejo API client.
|
||||
func NewClient(baseURL, adminUser, adminPassword string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
adminUser: adminUser,
|
||||
adminPassword: adminPassword,
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateForgejoUser creates a user in Forgejo and returns a personal access token.
|
||||
func (c *Client) CreateForgejoUser(ctx context.Context, username string) (string, error) {
|
||||
password, err := generatePassword(16)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate password: %w", err)
|
||||
}
|
||||
|
||||
email := username + "@spinoff.dev"
|
||||
if err := c.createUser(ctx, username, email, password); err != nil {
|
||||
return "", fmt.Errorf("create forgejo user: %w", err)
|
||||
}
|
||||
|
||||
token, err := c.createAccessToken(ctx, username, password, "dev-pod-access")
|
||||
if err != nil {
|
||||
_ = c.DeleteForgejoUser(ctx, username)
|
||||
return "", fmt.Errorf("create forgejo token: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// DeleteForgejoUser removes a user from Forgejo including all their repos.
|
||||
func (c *Client) DeleteForgejoUser(ctx context.Context, username string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete,
|
||||
fmt.Sprintf("%s/api/v1/admin/users/%s?purge=true", c.baseURL, username), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.SetBasicAuth(c.adminUser, c.adminPassword)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete user request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("delete user: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createUser(ctx context.Context, username, email, password string) error {
|
||||
payload := map[string]interface{}{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"must_change_password": false,
|
||||
"visibility": "private",
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
c.baseURL+"/api/v1/admin/users", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(c.adminUser, c.adminPassword)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusUnprocessableEntity {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("user already exists or invalid: %s", respBody)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("status %d: %s", resp.StatusCode, respBody)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createAccessToken(ctx context.Context, username, password, tokenName string) (string, error) {
|
||||
payload := map[string]interface{}{
|
||||
"name": tokenName,
|
||||
"scopes": []string{
|
||||
"write:repository",
|
||||
"write:issue",
|
||||
"write:organization",
|
||||
"write:user",
|
||||
"write:notification",
|
||||
"write:misc",
|
||||
"write:activitypub",
|
||||
"write:package",
|
||||
"read:repository",
|
||||
"read:issue",
|
||||
"read:organization",
|
||||
"read:user",
|
||||
"read:notification",
|
||||
"read:misc",
|
||||
"read:activitypub",
|
||||
"read:package",
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
fmt.Sprintf("%s/api/v1/users/%s/tokens", c.baseURL, username),
|
||||
bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("status %d: %s", resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
SHA1 string `json:"sha1"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
if result.SHA1 == "" {
|
||||
return "", fmt.Errorf("empty token in response")
|
||||
}
|
||||
return result.SHA1, nil
|
||||
}
|
||||
|
||||
// GetRepoFileContent reads a file from a Forgejo repository.
|
||||
// Returns the raw file content. Returns nil with no error if the file does not exist.
|
||||
func (c *Client) GetRepoFileContent(ctx context.Context, owner, repo, filepath, ref string) ([]byte, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents/%s", c.baseURL, owner, repo, filepath)
|
||||
if ref != "" {
|
||||
url += "?ref=" + ref
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.SetBasicAuth(c.adminUser, c.adminPassword)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Content string `json:"content"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
if result.Encoding != "base64" {
|
||||
return []byte(result.Content), nil
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(result.Content)
|
||||
if err != nil {
|
||||
// Forgejo sometimes uses standard base64 with newlines
|
||||
cleaned := strings.ReplaceAll(result.Content, "\n", "")
|
||||
decoded, err = base64.StdEncoding.DecodeString(cleaned)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode base64 content: %w", err)
|
||||
}
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// CreateIssueComment posts a comment on a Forgejo issue.
|
||||
func (c *Client) CreateIssueComment(ctx context.Context, owner, repo string, issueNumber int, body string) error {
|
||||
payload := map[string]string{"body": body}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/comments", c.baseURL, owner, repo, issueNumber)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payloadBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(c.adminUser, c.adminPassword)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("status %d: %s", resp.StatusCode, respBody)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generatePassword(length int) (string, error) {
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue