Adds the failing unit tests for keycloak's provider on the moved path: a grant-file consumer not moved should still be made and never held for retirement, and the moved harness should read MESH_RECEIVES as the grant path does. All three fail on an assertion against today's code, which wants nothing from the grant file on this path.
134 lines
5.1 KiB
Go
134 lines
5.1 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"
|
|
"git.novox.be/novox/mesh-sdk/go/provider/harness"
|
|
"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.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 = movedHarness(os.Getenv, provisioner{clients: OidcClients{KC: kc, Realm: realm}, guard: guard, announce: announce, log: logStderr})
|
|
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.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), harness.RetirementTools(h)...)); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// movedHarness is the harness the moved path shares with the SDK's helper: what main wires together, pulled out so
|
|
// it can be built and inspected without starting the process (novox/hq issue 1332).
|
|
func movedHarness(getenv func(string) string, adapter harness.Adapter) *harness.Harness {
|
|
return &harness.Harness{
|
|
Resource: ProvisionName,
|
|
Adapter: adapter,
|
|
Log: logStderr,
|
|
Announce: announce,
|
|
Node: getenv("MESH_NODE"),
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|