d78644d173
agentpr manages PRs/comments/whoami as unkin-agent (Vault AppRole -> gitea creds -> Gitea API), fixing tea's post-as-Ben default. watchpr polls PRs and alerts only on merge/close, human comment, CI failure, or lost mergeability. - cobra multi-binary layout mirroring node-lookup (cmd/ + internal/) - Makefile (build, patch|minor|major, completions, rpm), nfpm RPM with both binaries + bash/zsh/fish completions, woodpecker CI publishing to rpm-internal - unit tests for parsing, meaningful-change detection, and the Vault+Gitea client
84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// httpClient is shared by the Vault and Gitea calls. A modest timeout keeps a
|
|
// hung endpoint from wedging watchpr's poll loop.
|
|
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// approleLogin logs in with role_id only (no secret_id) and returns the
|
|
// resulting client_token.
|
|
func approleLogin(vaultAddr, roleID string) (string, error) {
|
|
body, _ := json.Marshal(map[string]string{"role_id": roleID})
|
|
url := strings.TrimRight(vaultAddr, "/") + "/v1/auth/approle/login"
|
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("vault approle login: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
data, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("vault approle login: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(data)))
|
|
}
|
|
|
|
var out struct {
|
|
Auth struct {
|
|
ClientToken string `json:"client_token"`
|
|
} `json:"auth"`
|
|
}
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return "", fmt.Errorf("vault approle login: decoding response: %w", err)
|
|
}
|
|
if out.Auth.ClientToken == "" {
|
|
return "", fmt.Errorf("vault approle login: no client_token in response")
|
|
}
|
|
return out.Auth.ClientToken, nil
|
|
}
|
|
|
|
// readGiteaCreds reads the Gitea creds secret and returns the token field.
|
|
func readGiteaCreds(vaultAddr, clientToken string) (string, error) {
|
|
url := strings.TrimRight(vaultAddr, "/") + "/v1/" + GiteaCredsPath
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("X-Vault-Token", clientToken)
|
|
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("vault read %s: %w", GiteaCredsPath, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
data, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("vault read %s: HTTP %d: %s", GiteaCredsPath, resp.StatusCode, strings.TrimSpace(string(data)))
|
|
}
|
|
|
|
var out struct {
|
|
Data struct {
|
|
Token string `json:"token"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return "", fmt.Errorf("vault read %s: decoding response: %w", GiteaCredsPath, err)
|
|
}
|
|
if out.Data.Token == "" {
|
|
return "", fmt.Errorf("vault read %s: no token field in secret", GiteaCredsPath)
|
|
}
|
|
return out.Data.Token, nil
|
|
}
|