The same agent in Go
The Go client is a standard-library-only module at client/go. It gives
you Client for the wire calls and Session, which remembers what you
read and fills in based_on. There is no run() in Go. You write the
loop yourself.
Set up the module
The import path is kslate.dev/kslate/client/go. It is its own module, so
it pulls none of the gateway’s dependencies:
go get kslate.dev/kslate/client/goIn the repository the same code is client/go.
The agent
package main
import (
"context"
"errors"
"fmt"
"time"
kslate "kslate.dev/kslate/client/go"
)
const record = "incidents/inc-8"
func main() {
ctx := context.Background()
client := kslate.New("http://127.0.0.1:8080", "rem-token")
session := client.NewSession() // remembers what it reads, fills in based_on
for {
rec, err := session.Get(ctx, record)
if err != nil {
panic(err)
}
plan := rec.Fields["planned_action"]
fmt.Printf("read planned_action v%d: %s\n", plan.Version, plan.Value)
time.Sleep(3 * time.Second) // your model call goes here
action := fmt.Sprintf("executed: %s", plan.Value)
_, err = session.Append(ctx, record, "actions_taken", action, "kubectl output", nil)
var stale *kslate.StalePremiseError
if errors.As(err, &stale) {
for _, m := range stale.Moved {
fmt.Printf("rejected: %s moved v%d -> v%d by %s\n", m.Field, m.YourVersion, m.CurrentVersion, m.CurrentAuthor)
}
continue // read again, decide again
}
if err != nil {
panic(err)
}
fmt.Println("write accepted")
return
}
}session.Getreads the record and remembers each field’s version.plan.Valueisjson.RawMessage. For a string field it prints with quotes. Unmarshal it if you need the Go value.session.Appendwithnilfor the last argument means “use what this session read asbased_on”. Pass an empty slice to send none.errors.Aspicks out the typed rejection. The write did not happen. The loop reads and decides again.
Run it
Same setup as your first agent: the gateway
running with diag-token and rem-token, and diagnoser.py changing
the plan while this program is deciding.
python3 diagnoser.py incidents/inc-8 1.5 &
sleep 0.7
go run .Output:
read planned_action v1: "restart payments-api pods"
rejected: planned_action moved v1 -> v2 by apikey:diagnoser
read planned_action v2: "roll back api deployment"
write acceptedAnd the record’s history:
v1 18:42:39 actions_taken apikey:remediator append APPLIED
evidence: kubectl output
based_on: planned_action@2
-- 18:42:36 actions_taken apikey:remediator append STALE_PREMISE
evidence: kubectl output
based_on: planned_action@1
v2 18:42:33 planned_action apikey:diagnoser put APPLIED
evidence: api logs show config parse error at 14:02:11
v1 18:42:32 planned_action apikey:diagnoser put APPLIED
evidence: api pods in CrashLoopBackOffWriting a versioned field
For a put, pass the version you read:
v := rec.Fields["status"].Version
why := "all checks green"
_, err = session.Put(ctx, record, "status", "resolved", kslate.PutOptions{
ExpectedVersion: &v,
Evidence: &why,
})
var staleV *kslate.StaleError
if errors.As(err, &staleV) {
// staleV.CurrentValue, staleV.CurrentAuthor, staleV.Diff
}In a pod
Use a token file instead of a key, so the credential rotates with the projected ServiceAccount token:
client := kslate.NewWithTokenSource("http://kslate-gateway.kslate.svc",
kslate.FileTokenSource("/var/run/secrets/kslate/token"))The rest of the API (Claim, Release, CheckpointSave,
CheckpointLoad, Query, HistoryPage) is listed in the
client reference.