Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add remote administration and local workflow runs

3abbe749096f4d7ff76f4d468e4427829b8a6f95 · Keenan Nemetz · 16d ago · parent 520470e1

Add remote administration and local workflow runs

Make `vole admin` the single namespace for user, repository, and access
management, runnable locally or over SSH by instance admins. Instance-admin
grants live in users.db, and the audit log names the actor separately from the
account being changed.

Add `vole workflow run` to execute workflow jobs in the working tree without a
server or runner. Positional arguments select workflows, --job selects jobs, and
the command reuses the push-time validator.

Update the README and guides for the new command shapes.

client/admin.go +112

@@ -0,0 +1,112 @@
1 package client
2
3 import (
4 "fmt"
5 "io"
6 "net/url"
7 "os"
8 "os/exec"
9 "strings"
10 )
11
12 // AdminExitError is a remote admin command that exited non-zero. It carries the
13 // code so the CLI can preserve a usage error instead of turning everything into
14 // exit 1.
15 type AdminExitError struct{ Code int }
16
17 func (e *AdminExitError) Error() string {
18 return fmt.Sprintf("remote admin command exited with status %d", e.Code)
19 }
20
21 // ExitCode lets main preserve the remote command's status without printing a
22 // second error over the one the server already wrote.
23 func (e *AdminExitError) ExitCode() int { return e.Code }
24
25 // AdminTarget turns a Git remote name or an SSH URL into the host and port the
26 // admin command should connect to. Unlike a Git remote, an admin URL may name
27 // only the instance: there is no repository path when you are creating one.
28 func AdminTarget(gitDir, remote string) (host, port string, err error) {
29 if remote == "" {
30 remote = "origin"
31 }
32 target, err := RemoteURL(gitDir, remote)
33 if err != nil {
34 return "", "", err
35 }
36 return ParseSSHServer(target)
37 }
38
39 // ParseSSHServer is ParseSSHRemote without the repository requirement. It
40 // accepts ssh://user@host:port[/path] and git's scp-like user@host:path form.
41 func ParseSSHServer(remote string) (host, port string, err error) {
42 if !strings.Contains(remote, "://") {
43 i := strings.Index(remote, ":")
44 if i < 0 {
45 return "", "", fmt.Errorf("%q is not an SSH address", remote)
46 }
47 host = remote[:i]
48 if host == "" {
49 return "", "", fmt.Errorf("%q has no host", remote)
50 }
51 return host, "", nil
52 }
53 u, err := url.Parse(remote)
54 if err != nil {
55 return "", "", fmt.Errorf("%q is not a URL: %w", remote, err)
56 }
57 if u.Scheme != "ssh" {
58 return "", "", fmt.Errorf("%q is not an SSH URL", remote)
59 }
60 if u.Hostname() == "" {
61 return "", "", fmt.Errorf("%q has no host", remote)
62 }
63 user := u.User.Username()
64 if user == "" {
65 user = "git"
66 }
67 return user + "@" + u.Hostname(), u.Port(), nil
68 }
69
70 // RunAdmin runs one `vole-admin` command over SSH. Output goes straight to the
71 // terminal; the server decides whether that output is human-readable text or
72 // JSON, based on jsonOut.
73 func RunAdmin(host, port string, args []string, jsonOut bool, stdin io.Reader) error {
74 remoteArgs := []string{"vole-admin"}
75 if jsonOut {
76 remoteArgs = append(remoteArgs, "--json")
77 }
78 remoteArgs = append(remoteArgs, args...)
79 quoted := make([]string, len(remoteArgs))
80 for i, arg := range remoteArgs {
81 quoted[i] = quoteSSHArg(arg)
82 }
83 cmd := SSHCommand(host, port, strings.Join(quoted, " "))
84 cmd.Stdin = stdin
85 cmd.Stdout = os.Stdout
86 cmd.Stderr = os.Stderr
87 if err := cmd.Run(); err != nil {
88 if ee, ok := err.(*exec.ExitError); ok {
89 return &AdminExitError{Code: ee.ExitCode()}
90 }
91 return err
92 }
93 return nil
94 }
95
96 // quoteSSHArg quotes one argument for the server's exec command parser. The
97 // server does not use a shell, so this only has to survive splitCommand.
98 func quoteSSHArg(s string) string {
99 if s != "" && !strings.ContainsAny(s, " \t\n'\"\\") {
100 return s
101 }
102 var b strings.Builder
103 b.WriteByte('"')
104 for _, r := range s {
105 if r == '\\' || r == '"' {
106 b.WriteByte('\\')
107 }
108 b.WriteRune(r)
109 }
110 b.WriteByte('"')
111 return b.String()
112 }