127 lines
4.7 KiB
Go
127 lines
4.7 KiB
Go
// keycloak-provider: keycloak's code, one binary the node's runtime launches and speaks MCP to over
|
|
// stdio through the Go SDK (novox/hq ADR 0188, 0193). It serves keycloak's tools and, beside them,
|
|
// runs long: the provisioner that makes keycloak the provider of the mesh `oidc-client` interface,
|
|
// and the guard that keeps the admin logging in with the mesh's secret (novox/hq issue 179).
|
|
//
|
|
// stdout is the MCP channel; everything this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
func say(format string, args ...any) { logStderr("[keycloak] "+format, args...) }
|
|
|
|
// announce emits an event without letting a broker hiccup fail what it announces: the change already
|
|
// happened in Keycloak.
|
|
func announce(event string, body map[string]any) {
|
|
if err := stdio.Emit(event, body); err != nil {
|
|
say("emit %s failed: %v", event, err)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
kc, err := ClientFromEnv(os.Getenv)
|
|
if err != nil {
|
|
// Without the admin password there is nothing to serve, provision or guard; said, not fatal,
|
|
// so the runtime does not restart a process that cannot do better.
|
|
say("%v; serving no tools and provisioning nothing", err)
|
|
if err := stdio.Serve("", nil); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
guard := &Guard{
|
|
KC: kc, Container: os.Getenv("MESH_KEYCLOAK_CONTAINER"),
|
|
Log: logStderr, Announce: announce,
|
|
}
|
|
kc.onRejected = guard.Nudge
|
|
go guard.Run(context.Background())
|
|
|
|
var h *Harness
|
|
if issuer, err := Issuer(os.Getenv); err != nil {
|
|
say("%v — the provisioner cannot run without it", err)
|
|
} else if realm, err := RealmOf(issuer); err != nil {
|
|
say("%v — the provisioner cannot run without it", err)
|
|
} else if consumers := handedConsumersFile(os.Getenv, os.Stat); consumers != "" {
|
|
// Handed to the provisioner (novox/hq ADR 0373): the provider helper answers the consumers its
|
|
// machine's client fills the file with; the grant directory is not read. Retirement is the
|
|
// harness's, never run, so its guards and its tools stand (issue 805).
|
|
h = &Harness{
|
|
Resource: ProvisionName,
|
|
Adapter: provisioner{clients: OidcClients{KC: kc, Realm: realm}, guard: guard, announce: announce, log: logStderr},
|
|
Log: logStderr,
|
|
Announce: announce,
|
|
Node: os.Getenv("MESH_NODE"),
|
|
}
|
|
run, p, bus, err := moved(consumers, h, issuer)
|
|
if err != nil {
|
|
say("%v", err)
|
|
} else {
|
|
go func() {
|
|
if err := run.Run(context.Background(), p, bus); err != nil {
|
|
say("the provider helper stopped: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
} else if receives := os.Getenv("MESH_RECEIVES"); receives == "" {
|
|
say("MESH_RECEIVES is not set — the provisioner cannot run without it")
|
|
} else {
|
|
h = &Harness{
|
|
Resource: "oidc-client",
|
|
Receives: receives,
|
|
Adapter: provisioner{clients: OidcClients{KC: kc, Realm: realm}, guard: guard, announce: announce, log: logStderr},
|
|
Log: logStderr,
|
|
// A consumer failed for minutes is said on the bus, where the controller hears it and
|
|
// `status` names it (novox/hq ADR 0224).
|
|
Announce: announce,
|
|
Node: os.Getenv("MESH_NODE"),
|
|
}
|
|
go h.Run(context.Background())
|
|
}
|
|
// Started on the grant path while the module is to be handed, it restarts itself when its client first fills
|
|
// the consumers' file, so the first fill moves it onto the helper without a hand restart (issue 805): `restart-on`
|
|
// names a service or a container, and the only one here is Keycloak's own.
|
|
if at := os.Getenv("MESH_CONSUMERS_FILE"); at != "" && handedConsumersFile(os.Getenv, os.Stat) == "" {
|
|
go exitWhenFilled(at, os.Stat, 5*time.Second, func() {
|
|
say("%s is filled: restarting to answer from it", at)
|
|
os.Exit(0)
|
|
})
|
|
}
|
|
// The retirement tools beside keycloak's own (novox/hq ADR 0230).
|
|
if err := stdio.Serve("", append(Tools(kc, guard), RetirementTools(h)...)); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// handedConsumersFile is the consumers' file when keycloak is handed to the provisioner: the file MESH_CONSUMERS_FILE
|
|
// names, which the controller places only where it hands the module. Empty, where it is not set or the file is not
|
|
// there, and keycloak then reads the grant directory exactly as it always did (issue 805).
|
|
func handedConsumersFile(getenv func(string) string, stat func(string) (os.FileInfo, error)) string {
|
|
at := getenv("MESH_CONSUMERS_FILE")
|
|
if at == "" {
|
|
return ""
|
|
}
|
|
if _, err := stat(at); err != nil {
|
|
return ""
|
|
}
|
|
return at
|
|
}
|
|
|
|
// exitWhenFilled calls filled once the file is there, asking every so often.
|
|
func exitWhenFilled(at string, stat func(string) (os.FileInfo, error), every time.Duration, filled func()) {
|
|
for {
|
|
if _, err := stat(at); err == nil {
|
|
filled()
|
|
return
|
|
}
|
|
time.Sleep(every)
|
|
}
|
|
}
|