Skip to content
The same agent in Go

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/go

In 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.Get reads the record and remembers each field’s version.
  • plan.Value is json.RawMessage. For a string field it prints with quotes. Unmarshal it if you need the Go value.
  • session.Append with nil for the last argument means “use what this session read as based_on”. Pass an empty slice to send none.
  • errors.As picks 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 accepted

And 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 CrashLoopBackOff

Writing 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.