127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
// forge-account-provider: gitea as the provider of `forge-account` (novox/hq ADR 0399, ADR 0400; issue 955). A process
|
|
// the node's runtime launches and speaks MCP to over stdio, beside the module's other bundles.
|
|
//
|
|
// For each machine whose ssh-client binds the provision and has reported its public key (ssh-client's state `keys`),
|
|
// it makes the machine's agent a forge account of its own (agent-<node>, in the agents team), registers the key for
|
|
// it, and serves where and as whom to reach the forge over ssh: the user, the forge machine's mesh name and port, its
|
|
// host key and the two url roots. A public key is no secret, so the provision has no credential.
|
|
//
|
|
// stdout is the MCP channel; it says what it says on stderr, and never the admin's password.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/provider"
|
|
|
|
"gitea/internal/forgehttp"
|
|
)
|
|
|
|
func say(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "[forge-account] "+format+"\n", args...)
|
|
}
|
|
|
|
// execRunner runs a program on this machine.
|
|
type execRunner struct{}
|
|
|
|
func (execRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
|
defer cancel()
|
|
out, err := exec.CommandContext(ctx, name, args...).Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// stateKeys is ssh-client's state `keys` over the runtime (the manifest's `reads` grants it).
|
|
type stateKeys struct{}
|
|
|
|
func (stateKeys) Watch(handle func(key, op string, value []byte, current bool)) error {
|
|
return stdio.State("ssh-client.keys").Watch("", func(c stdio.StateChange) error {
|
|
handle(c.Key, c.Op, c.Value, c.Current)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// configFromEnv is the service's environment as the manifest hands it, naming what is missing.
|
|
func configFromEnv(getenv func(string) string) (Config, error) {
|
|
port, perr := strconv.Atoi(getenv("MESH_GITEA_SSH_PORT"))
|
|
cfg := Config{
|
|
Node: getenv("MESH_NODE"),
|
|
HostFile: getenv("MESH_FORGE_HOST_FILE"),
|
|
OriginsFile: getenv("MESH_GITEA_ORIGINS_FILE"),
|
|
Port: port,
|
|
Forge: forgehttp.Client{URL: strings.TrimRight(getenv("MESH_GITEA_URL"), "/"), User: getenv("MESH_GITEA_ADMIN_USER"),
|
|
PasswordFile: getenv("MESH_GITEA_ADMIN_PASSWORD_FILE"), Timeout: 20 * time.Second},
|
|
Runner: execRunner{},
|
|
}
|
|
var missing []string
|
|
for _, kv := range [][2]string{{"MESH_NODE", cfg.Node}, {"MESH_FORGE_HOST_FILE", cfg.HostFile},
|
|
{"MESH_GITEA_ORIGINS_FILE", cfg.OriginsFile}, {"MESH_GITEA_URL", cfg.Forge.URL}, {"MESH_GITEA_ADMIN_USER", cfg.Forge.User},
|
|
{"MESH_GITEA_ADMIN_PASSWORD_FILE", cfg.Forge.PasswordFile}} {
|
|
if kv[1] == "" {
|
|
missing = append(missing, kv[0])
|
|
}
|
|
}
|
|
if perr != nil || port <= 0 {
|
|
missing = append(missing, "MESH_GITEA_SSH_PORT")
|
|
}
|
|
if len(missing) > 0 {
|
|
return Config{}, fmt.Errorf("the forge-account provider cannot start: %s not set by the manifest", strings.Join(missing, ", "))
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func main() {
|
|
cfg, err := configFromEnv(os.Getenv)
|
|
if err != nil {
|
|
// Said, not fatal: the runtime would restart a process that cannot do better.
|
|
say("%v; providing nothing", err)
|
|
if err := stdio.Serve("", nil); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
cfg.Log = say
|
|
svc := NewService(cfg)
|
|
p := svc.Provider(provider.StateBus(Module))
|
|
ctx := context.Background()
|
|
// A pass answers every consumer; one at a time, and one per change of a machine's key.
|
|
var passing sync.Mutex
|
|
pass := func() {
|
|
passing.Lock()
|
|
defer passing.Unlock()
|
|
if _, err := p.Pass(ctx); err != nil {
|
|
say("a pass failed: %v", err)
|
|
}
|
|
}
|
|
svc.OnKeys = func(string) { go pass() }
|
|
go func() {
|
|
if err := svc.WatchKeys(ctx, stateKeys{}); err != nil {
|
|
say("the machines' keys are not watched: %v", err)
|
|
return
|
|
}
|
|
say("watching the machines' keys")
|
|
pass()
|
|
}()
|
|
go func() {
|
|
if err := p.Run(ctx); err != nil {
|
|
say("the provider stopped: %v", err)
|
|
}
|
|
}()
|
|
if err := stdio.Serve("", nil); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|