Files
mesh-catalog/modules/gitea/cmd/forge-account-provider/main.go
T

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)
}
}