package adminrpc
import (
"encoding/json"
"errors"
"net"
"time"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/admin"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/gitexec"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/store"
)
// Client implements admin.Ops by calling a running gitfed-server's admin
// socket. One connection is opened per call — simple, and admin operations
// are rare enough that the overhead doesn't matter.
type Client struct {
socketPath string
timeout time.Duration
}
var _ admin.Ops = (*Client)(nil)
func NewClient(socketPath string) *Client {
return &Client{socketPath: socketPath, timeout: 5 * time.Second}
}
// Ping checks whether a gitfed-server is listening on the socket.
func (c *Client) Ping() error {
return c.call(methodListUsers, nil, nil)
}
func (c *Client) call(method string, args any, out any) error {
return c.callWithTimeout(method, args, out, c.timeout)
}
// callWithTimeout is call with an explicit deadline, for the rare RPC method
// (currently just ImportRepo) whose underlying operation can legitimately
// run far longer than every other admin call, which are all local and
// near-instant.
func (c *Client) callWithTimeout(method string, args any, out any, timeout time.Duration) error {
conn, err := net.DialTimeout("unix", c.socketPath, timeout)
if err != nil {
return err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(timeout))
if err := json.NewEncoder(conn).Encode(request{Method: method, Args: args}); err != nil {
return err
}
var resp struct {
Result json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
if err := json.NewDecoder(conn).Decode(&resp); err != nil {
return err
}
if resp.Error != "" {
// Reconstruct sentinel errors by message so identity comparisons
// like `err == store.ErrNotFound` still work across the RPC
// boundary — JSON only carries the error string, and a fresh
// errors.New(resp.Error) would never compare equal to the
// original sentinel even with the same text.
if resp.Error == store.ErrNotFound.Error() {
return store.ErrNotFound
}
return errors.New(resp.Error)
}
if out != nil && len(resp.Result) > 0 {
return json.Unmarshal(resp.Result, out)
}
return nil
}
func (c *Client) ListUsers() ([]store.User, error) {
var out listUsersResult
err := c.call(methodListUsers, nil, &out)
return out.Users, err
}
func (c *Client) CreateUser(username, pubKeyAuthorized string) error {
err := c.call(methodCreateUser, userKeyArgs{Username: username, PubKey: pubKeyAuthorized}, nil)
return err
}
func (c *Client) AddUserKey(username, pubKeyAuthorized string) error {
err := c.call(methodAddUserKey, userKeyArgs{Username: username, PubKey: pubKeyAuthorized}, nil)
return err
}
func (c *Client) RemoveUserKey(username, pubKeyAuthorized string) error {
return c.call(methodRemoveUserKey, userKeyArgs{Username: username, PubKey: pubKeyAuthorized}, nil)
}
func (c *Client) DeleteUser(username string) error {
err := c.call(methodDeleteUser, nameArgs{Name: username}, nil)
return err
}
func (c *Client) ListRepos() ([]store.Repo, error) {
var out listReposResult
err := c.call(methodListRepos, nil, &out)
return out.Repos, err
}
func (c *Client) GetRepo(name string) (store.Repo, error) {
var out store.Repo
err := c.call(methodGetRepo, nameArgs{Name: name}, &out)
return out, err
}
func (c *Client) CreateRepo(name, ownerUsername string) error {
err := c.call(methodCreateRepo, createRepoArgs{Name: name, Owner: ownerUsername}, nil)
return err
}
// importRPCTimeout gives the RPC call itself a bit more headroom than
// admin.ImportCloneTimeout, so the server-side clone's own timeout is always
// what actually fires first — the client-side deadline is a backstop, not
// the primary bound.
const importRPCTimeout = admin.ImportCloneTimeout + 30*time.Second
func (c *Client) ImportRepo(name, sourceURL, ownerUsername string) error {
return c.callWithTimeout(methodImportRepo, importRepoArgs{Name: name, SourceURL: sourceURL, Owner: ownerUsername}, nil, importRPCTimeout)
}
// archiveRPCTimeout gives git archive (a local, CPU-bound tar/zip of
// however much history+content ref pulls in, up to archiveMaxBytes) more
// headroom than the default 5s every other, near-instant admin call gets.
const archiveRPCTimeout = 60 * time.Second
func (c *Client) GetRepoArchive(name, ref, format string) ([]byte, error) {
var out archiveResult
err := c.callWithTimeout(methodGetRepoArchive, archiveArgs{Name: name, Ref: ref, Format: format}, &out, archiveRPCTimeout)
return out.Data, err
}
func (c *Client) DeleteRepo(name string) error {
err := c.call(methodDeleteRepo, nameArgs{Name: name}, nil)
return err
}
func (c *Client) GetACL(repoName string) (store.ACL, error) {
var out store.ACL
err := c.call(methodGetACL, nameArgs{Name: repoName}, &out)
return out, err
}
func (c *Client) GrantCollaborator(repoName, principal, actor string, role store.Role) error {
err := c.call(methodGrantCollaborator, collaboratorArgs{Repo: repoName, Principal: principal, Actor: actor, Role: role}, nil)
return err
}
func (c *Client) RevokeCollaborator(repoName, principal string) error {
err := c.call(methodRevokeCollaborator, collaboratorArgs{Repo: repoName, Principal: principal}, nil)
return err
}
func (c *Client) ListTrustedCAs() ([]store.TrustedCA, error) {
var out listTrustResult
err := c.call(methodListTrustedCAs, nil, &out)
return out.Trust, err
}
func (c *Client) CountPendingTrust() (int, error) {
var out countResult
err := c.call(methodCountPendingTrust, nil, &out)
return out.Count, err
}
func (c *Client) ApproveDomain(domain string) error {
err := c.call(methodApproveDomain, domainArgs{Domain: domain}, nil)
return err
}
func (c *Client) ListAudit(limit int) ([]store.AuditEvent, error) {
var out listAuditResult
err := c.call(methodListAudit, limitArgs{Limit: limit}, &out)
return out.Events, err
}
func (c *Client) SetRepoPublic(name string, public bool) error {
return c.call(methodSetRepoPublic, setPublicArgs{Name: name, Public: public}, nil)
}
func (c *Client) SetRepoTopics(name string, topics []string) error {
return c.call(methodSetRepoTopics, setTopicsArgs{Name: name, Topics: topics}, nil)
}
func (c *Client) SetRepoDescription(name, description string) error {
return c.call(methodSetRepoDescription, setDescriptionArgs{Name: name, Description: description}, nil)
}
func (c *Client) GetRepoReadme(name string) (string, bool, error) {
var out readmeResult
err := c.call(methodGetRepoReadme, nameArgs{Name: name}, &out)
return out.Content, out.Found, err
}
func (c *Client) ListRepoReadmeLanguages(name string) ([]string, error) {
var out readmeLangsResult
err := c.call(methodListReadmeLangs, nameArgs{Name: name}, &out)
return out.Langs, err
}
func (c *Client) GetRepoReadmeLang(name, lang string) (string, bool, error) {
var out readmeResult
err := c.call(methodGetRepoReadmeLang, nameLangArgs{Name: name, Lang: lang}, &out)
return out.Content, out.Found, err
}
func (c *Client) GetRepoLicense(name string) (string, string, bool, error) {
var out licenseResult
err := c.call(methodGetRepoLicense, nameArgs{Name: name}, &out)
return out.Content, out.Filename, out.Found, err
}
func (c *Client) ListRepoTags(name string) ([]string, error) {
var out listTagsResult
err := c.call(methodListRepoTags, nameArgs{Name: name}, &out)
return out.Tags, err
}
func (c *Client) ListRepoTree(name, path string) ([]gitexec.TreeEntry, bool, error) {
var out listTreeResult
err := c.call(methodListRepoTree, pathArgs{Name: name, Path: path}, &out)
return out.Entries, out.Found, err
}
func (c *Client) ListRepoTreeAtRef(name, ref, path string) ([]gitexec.TreeEntry, bool, error) {
var out listTreeResult
err := c.call(methodListRepoTreeAtRef, pathRefArgs{Name: name, Ref: ref, Path: path}, &out)
return out.Entries, out.Found, err
}
func (c *Client) GetRepoFile(name, path string) (string, bool, error) {
var out fileResult
err := c.call(methodGetRepoFile, pathArgs{Name: name, Path: path}, &out)
return out.Content, out.Found, err
}
func (c *Client) GetRepoFileAtRef(name, ref, path string) (string, bool, error) {
var out fileResult
err := c.call(methodGetRepoFileAtRef, pathRefArgs{Name: name, Ref: ref, Path: path}, &out)
return out.Content, out.Found, err
}
func (c *Client) GetRepoBranch(name string) (string, bool, error) {
var out branchResult
err := c.call(methodGetRepoBranch, nameArgs{Name: name}, &out)
return out.Branch, out.Found, err
}
func (c *Client) CountCommits(name string) (int, bool, error) {
var out countFoundResult
err := c.call(methodCountCommits, nameArgs{Name: name}, &out)
return out.Count, out.Found, err
}
func (c *Client) CountContributors(name string) (int, bool, error) {
var out countFoundResult
err := c.call(methodCountContributors, nameArgs{Name: name}, &out)
return out.Count, out.Found, err
}
func (c *Client) DominantLanguage(name string) (string, bool, error) {
var out languageResult
err := c.call(methodDominantLanguage, nameArgs{Name: name}, &out)
return out.Language, out.Found, err
}
func (c *Client) RepoDiskUsage(name string) (int64, error) {
var out diskUsageResult
err := c.call(methodRepoDiskUsage, nameArgs{Name: name}, &out)
return out.Bytes, err
}
func (c *Client) GetUser(username string) (store.User, error) {
var out userResult
err := c.call(methodGetUser, nameArgs{Name: username}, &out)
return out.User, err
}
func (c *Client) SetUserAdmin(username string, isAdmin bool) error {
return c.call(methodSetUserAdmin, setAdminArgs{Username: username, IsAdmin: isAdmin}, nil)
}
func (c *Client) SetUserBio(username, bio string) error {
return c.call(methodSetUserBio, setBioArgs{Username: username, Bio: bio}, nil)
}
func (c *Client) SetPassword(username, newPassword string) error {
return c.call(methodSetPassword, setPasswordArgs{Username: username, Password: newPassword}, nil)
}
func (c *Client) VerifyPassword(username, password string) (bool, bool, error) {
var out verifyPasswordResult
err := c.call(methodVerifyPassword, setPasswordArgs{Username: username, Password: password}, &out)
return out.IsAdmin, out.OK, err
}
func (c *Client) CreateSession(principal, username string, isAdmin bool) (string, error) {
var out createSessionResult
err := c.call(methodCreateSession, createSessionArgs{Principal: principal, Username: username, IsAdmin: isAdmin}, &out)
return out.Token, err
}
func (c *Client) GetSession(token string) (store.Session, error) {
var out sessionResult
err := c.call(methodGetSession, tokenArgs{Token: token}, &out)
return out.Session, err
}
func (c *Client) DeleteSession(token string) error {
return c.call(methodDeleteSession, tokenArgs{Token: token}, nil)
}
func (c *Client) CheckAccess(repoName, principal string, want store.Role) (store.Role, bool, error) {
var out checkAccessResult
err := c.call(methodCheckAccess, checkAccessArgs{Repo: repoName, Principal: principal, Want: want}, &out)
return out.Role, out.OK, err
}
func (c *Client) ListCommits(name string, limit int) ([]gitexec.Commit, bool, error) {
var out listCommitsResult
err := c.call(methodListCommits, nameLimitArgs{Name: name, Limit: limit}, &out)
return out.Commits, out.Found, err
}
func (c *Client) ListCommitsPage(name string, limit, offset int) ([]gitexec.Commit, bool, error) {
var out listCommitsResult
err := c.call(methodListCommitsPage, nameLimitOffsetArgs{Name: name, Limit: limit, Offset: offset}, &out)
return out.Commits, out.Found, err
}
func (c *Client) ShowCommit(name, hash string) (gitexec.CommitDetail, bool, error) {
var out showCommitResult
err := c.call(methodShowCommit, nameHashArgs{Name: name, Hash: hash}, &out)
return out.Detail, out.Found, err
}
func (c *Client) CommitDiff(name, hash string) (string, bool, error) {
var out commitDiffResult
err := c.call(methodCommitDiff, nameHashArgs{Name: name, Hash: hash}, &out)
return out.Diff, out.Truncated, err
}
func (c *Client) ListBranches(name string) ([]string, error) {
var out listBranchesResult
err := c.call(methodListBranches, nameArgs{Name: name}, &out)
return out.Branches, err
}
func (c *Client) CreateMergeRequest(repoName, title, description, author, sourceBranch, targetBranch string) (store.MergeRequest, error) {
var out mrResult
err := c.call(methodCreateMergeRequest, createMRArgs{
Repo: repoName, Title: title, Description: description, Author: author,
SourceBranch: sourceBranch, TargetBranch: targetBranch,
}, &out)
return out.MR, err
}
func (c *Client) ListMergeRequests(repoName string) ([]store.MergeRequest, error) {
var out listMRResult
err := c.call(methodListMergeRequests, nameArgs{Name: repoName}, &out)
return out.MRs, err
}
func (c *Client) GetMergeRequest(repoName string, number int) (store.MergeRequest, error) {
var out mrResult
err := c.call(methodGetMergeRequest, mrArgs{Repo: repoName, Number: number}, &out)
return out.MR, err
}
func (c *Client) MergeRequestDiff(repoName string, number int) ([]gitexec.DiffFile, string, bool, bool, error) {
var out mrDiffResult
err := c.call(methodMergeRequestDiff, mrArgs{Repo: repoName, Number: number}, &out)
return out.Files, out.Diff, out.Truncated, out.Found, err
}
func (c *Client) CheckMergeRequestMergeable(repoName string, number int) (gitexec.MergeResult, error) {
var out mergeResultRPC
err := c.call(methodCheckMergeRequestMergeable, mrArgs{Repo: repoName, Number: number}, &out)
return out.Result, err
}
func (c *Client) MergeMergeRequest(repoName string, number int, actor string) (store.MergeRequest, gitexec.MergeResult, error) {
var out mergeMRResult
err := c.call(methodMergeMergeRequest, mergeMRArgs{Repo: repoName, Number: number, Actor: actor}, &out)
return out.MR, out.Result, err
}
func (c *Client) CloseMergeRequest(repoName string, number int) error {
return c.call(methodCloseMergeRequest, mrArgs{Repo: repoName, Number: number}, nil)
}
func (c *Client) ListMRComments(repoName string, number int) ([]store.MRComment, error) {
var out listMRCommentsResult
err := c.call(methodListMRComments, mrArgs{Repo: repoName, Number: number}, &out)
return out.Comments, err
}
func (c *Client) AddMRComment(repoName string, number int, author, body string) (store.MRComment, error) {
var out addMRCommentResult
err := c.call(methodAddMRComment, mrCommentArgs{Repo: repoName, Number: number, Author: author, Body: body}, &out)
return out.Comment, err
}
func (c *Client) PinRepo(principal, domain, repo, label string) error {
return c.call(methodPinRepo, pinArgs{Principal: principal, Domain: domain, Repo: repo, Label: label}, nil)
}
func (c *Client) UnpinRepo(principal, domain, repo string) error {
return c.call(methodUnpinRepo, pinArgs{Principal: principal, Domain: domain, Repo: repo}, nil)
}
func (c *Client) ListPinnedRepos(principal string) ([]store.PinnedRepo, error) {
var out listPinsResult
err := c.call(methodListPinnedRepos, principalArgs{Principal: principal}, &out)
return out.Pins, err
}
func (c *Client) ListNotifications(principal string) ([]store.Notification, error) {
var out listNotifsResult
err := c.call(methodListNotifications, principalArgs{Principal: principal}, &out)
return out.Notifications, err
}
func (c *Client) CountPendingNotifications(principal string) (int, error) {
var out countResult
err := c.call(methodCountPendingNotifs, principalArgs{Principal: principal}, &out)
return out.Count, err
}
func (c *Client) AcceptNotification(principal, id string) error {
return c.call(methodAcceptNotification, notificationIDArgs{Principal: principal, ID: id}, nil)
}
func (c *Client) DismissNotification(principal, id string) error {
return c.call(methodDismissNotification, notificationIDArgs{Principal: principal, ID: id}, nil)
}