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.
|
6e1fca51
Keenan Nemetz
6d ago
|
1 | package agent |
| 2 | ||
| 3 | // The environment a job acting as a bot gets. | |
| 4 | // | |
| 5 | // The credential is a file in the run directory, and git and the `vole` CLI both | |
| 6 | // already honour an ssh command, so a step needs no special handling: `git push` and | |
| 7 | // `vole issue comment` authenticate as the account because the environment points at | |
| 8 | // the key. The names are reserved in the workflow format, so a workflow cannot | |
| 9 | // replace them. | |
| 10 | ||
| 11 | import ( | |
| 12 | "fmt" | |
| 13 | "net/url" | |
| 14 | "strings" | |
| 15 | ) | |
| 16 | ||
| 17 | // RunCredentialPath is where the key is written inside the job's container. | |
| 18 | const RunCredentialPath = "/run/vole/id_bot" | |
| 19 | ||
| 20 | // RunKnownHostsPath is where the instance's host key is written. | |
| 21 | const RunKnownHostsPath = "/run/vole/known_hosts" | |
| 22 | ||
| 23 | // withCredentialEnv returns a copy of the job's environment with the variables that | |
| 24 | // point git and the CLI at the run credential. It copies rather than editing the | |
| 25 | // pinned map, which belongs to the run record. | |
| 26 | func withCredentialEnv(env map[string]string, remote string, job *runnerJob) (map[string]string, error) { | |
| 27 | sshCommand, err := credentialSSHCommand(remote) | |
| 28 | if err != nil { | |
| 29 | return nil, err | |
| 30 | } | |
| 31 | out := make(map[string]string, len(env)+5) | |
| 32 | for name, value := range env { | |
| 33 | out[name] = value | |
| 34 | } | |
| 35 | out["VOLE_RUN_ID"] = job.ID | |
| 36 | out["VOLE_REPO"] = job.Repo | |
| 37 | out["VOLE_SSH_COMMAND"] = sshCommand | |
| 38 | out["GIT_SSH_COMMAND"] = sshCommand | |
| 39 | out["VOLE_KNOWN_HOSTS"] = RunKnownHostsPath | |
| 40 | return out, nil | |
| 41 | } | |
| 42 | ||
| 43 | // credentialSSHCommand pins the key and the known_hosts file. IdentitiesOnly keeps | |
| 44 | // ssh from offering an agent key that happens to be in the container, so the run | |
| 45 | // credential is the only identity a step can present. | |
| 46 | func credentialSSHCommand(remote string) (string, error) { | |
| 47 | if _, _, err := splitRemote(remote); err != nil { | |
| 48 | return "", err | |
| 49 | } | |
| 50 | return "ssh -i " + RunCredentialPath + " -o IdentitiesOnly=yes -o UserKnownHostsFile=" + RunKnownHostsPath, nil | |
| 51 | } | |
| 52 | ||
| 53 | // knownHostsLine is the entry that lets the job verify the instance it pushes to. | |
| 54 | // OpenSSH brackets a host whose port is not the default. | |
| 55 | func knownHostsLine(remote, hostKey string) (string, error) { | |
| 56 | host, port, err := splitRemote(remote) | |
| 57 | if err != nil { | |
| 58 | return "", err | |
| 59 | } | |
| 60 | if strings.TrimSpace(hostKey) == "" { | |
| 61 | return "", fmt.Errorf("the instance sent no host key with the job, so the credential has nothing to verify it against") | |
| 62 | } | |
| 63 | if port == "" || port == "22" { | |
| 64 | return host + " " + strings.TrimSpace(hostKey) + "\n", nil | |
| 65 | } | |
| 66 | return "[" + host + "]:" + port + " " + strings.TrimSpace(hostKey) + "\n", nil | |
| 67 | } | |
| 68 | ||
| 69 | func splitRemote(remote string) (host, port string, err error) { | |
| 70 | u, err := url.Parse(remote) | |
| 71 | if err != nil || u.Scheme != "ssh" || u.Hostname() == "" { | |
| 72 | return "", "", fmt.Errorf("%q is not an ssh URL, so the job cannot be told how to reach the instance", remote) | |
| 73 | } | |
| 74 | return u.Hostname(), u.Port(), nil | |
| 75 | } |