Files
unkin-agent 26cd05e961
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Make the Vault gitea creds path selectable
agentpr always read gitea/creds/unkin-agent from a bare const, so a service
like repospawner could not run it as its own Gitea identity.

- Replace the GiteaCredsPath const with a function: GITEA_CREDS_PATH when set,
  otherwise gitea/creds/<AGENT_LOGIN>. Unset env still resolves to
  gitea/creds/unkin-agent, so existing callers are unchanged.
- Thread the creds path through fetchGiteaToken/readGiteaCreds instead of
  reading a package-level const, and report it in the error messages.
- Make agentpr's help text login-agnostic and document both variables.
2026-08-30 00:50:35 +10:00

85 lines
2.5 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 func() { _ = 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 at credsPath and returns the
// token field.
func readGiteaCreds(vaultAddr, clientToken, credsPath string) (string, error) {
url := strings.TrimRight(vaultAddr, "/") + "/v1/" + credsPath
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", credsPath, err)
}
defer func() { _ = resp.Body.Close() }()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("vault read %s: HTTP %d: %s", credsPath, 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", credsPath, err)
}
if out.Data.Token == "" {
return "", fmt.Errorf("vault read %s: no token field in secret", credsPath)
}
return out.Data.Token, nil
}