Files
zeekayandHanzo Dev 8f643abe3b kms-cli holds no Kubernetes client
Two call sites, two different reasons they were wrong.

`bootstrap --output k8-secret` dialled the cluster and created-or-updated a
corev1.Secret from a Go template. Nothing invokes it — zero references across
hanzo/kms, hanzo/kms-operator and hanzo/universe — and writing per-service
Secrets is the kms-operator's job, not a CLI's. Deleted, along with --output
and the three --k8-secret-* flags: one legal output value is not a choice.

`pam kubernetes access` never dialled a cluster at all. It used clientcmd to
splice a cluster/user/context into the operator's own ~/.kube/config, flip
current-context, and revert on shutdown. It now writes a self-contained session
kubeconfig (0600, removed on shutdown) and prints KUBECONFIG=<path>. That is a
net code decrease — the whole revert block goes — and it closes the three TODOs
the original left admitting it took no lock: a SIGKILLed proxy or two concurrent
sessions could leave the real kubeconfig pointing at a dead port. It is never
read or written now.

Verified by rendering one and running kubectl config view --minify against it,
which resolves only if current-context, cluster and user cross-reference.

go.mod loses every k8s.io/* entry (-30 go.mod, -65 go.sum).

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 19:05:02 -07:00

242 lines
6.4 KiB
Go

package redis
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"sync/atomic"
"time"
"github.com/hanzokms/cli/packages/pam/session"
"github.com/rs/zerolog/log"
"github.com/smallnest/resp3"
)
// RelayHandler handles relaying commands and responses between client and server
type RelayHandler struct {
clientToSelfConn *RedisConn
selfToServerConn *RedisConn
sessionLogger session.SessionLogger
}
type LogType string
const (
LogTypeCmd LogType = "cmd"
LogTypePush LogType = "push"
LogTypeMonitor LogType = "monitor"
)
type RedisLogEntry struct {
LogType LogType `json:"type"`
Cmd interface{} `json:"cmd,omitempty"`
}
type serverReply struct {
value *resp3.Value
err error
}
// NewRelayHandler creates a new relay handler
func NewRelayHandler(clientToSelfConn *RedisConn, selfToServerConn *RedisConn, sessionLogger session.SessionLogger) *RelayHandler {
return &RelayHandler{
clientToSelfConn: clientToSelfConn,
selfToServerConn: selfToServerConn,
sessionLogger: sessionLogger,
}
}
func (h *RelayHandler) Handle(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
monitorMode := atomic.Bool{}
serverReplyCh := make(chan serverReply, 1)
go func(ch chan<- serverReply) {
for {
if err := ctx.Err(); err != nil {
return
}
if err := h.selfToServerConn.conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
log.Error().Err(err).Msg("failed to set read deadline")
ch <- serverReply{nil, err}
return
}
v, _, err := h.selfToServerConn.Reader().ReadValue()
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
continue
}
log.Error().Err(err).Msg("Error reading from server")
ch <- serverReply{nil, err}
return
}
if monitorMode.Load() {
// In the monitoring mode, the server will keep sending simple string log values like
//
// +1766623914.688074 [0 127.0.0.1:33956] "AUTH" "(redacted)"
//
// We need to forward them all to the client. Other than that, we treat them all as
// server reply
if v.Type == resp3.TypeSimpleString && v.Str != "OK" {
err = h.clientToSelfConn.WriteValue(v, true)
if err != nil {
log.Error().Err(err).Msg("Error forwarding monitoring logs to the client")
ch <- serverReply{nil, err}
return
}
h.writeLogEntry(LogTypeMonitor, nil, v)
continue
}
} else if (v.Type == resp3.TypeArray && len(v.Elems) > 0 && strings.ToLower(v.Elems[0].Str) == "message") ||
(v.Type == resp3.TypePush) {
// pubsub in resp2/resp3 mode will send a push as the confirmation instead of return anything,
// we need to treat that as a cmd reply otherwise the main loop will wait forever for the
// server reply to forward
if !isPubSubConfirmation(v) {
err = h.clientToSelfConn.WriteValue(v, true)
if err != nil {
log.Error().Err(err).Msg("Error forwarding push messages to the client")
ch <- serverReply{nil, err}
return
}
h.writeLogEntry(LogTypePush, nil, v)
continue
}
}
select {
case ch <- serverReply{v, nil}:
case <-ctx.Done():
return
}
}
}(serverReplyCh)
for {
if ctx.Err() != nil {
return ctx.Err()
}
err := h.clientToSelfConn.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
return err
}
value, _, err := h.clientToSelfConn.Reader().ReadValue()
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
continue
}
return err
}
switch value.Type {
case resp3.TypeArray:
cmd := value.Elems[0]
if cmd.Type != resp3.TypeBlobString {
return fmt.Errorf("expected BlobString, got %c", cmd.Type)
}
cmdStr := strings.ToLower(value.Elems[0].Str)
switch cmdStr {
// Handle auth command, we just reply OK instead of forwarding it to the server
case "auth":
err := h.clientToSelfConn.WriteValue(&resp3.Value{Type: resp3.TypeSimpleString, Str: "OK"}, true)
if err != nil {
return err
}
break
// Forward all other commands
default:
err := h.selfToServerConn.WriteValue(value, true)
if err != nil {
h.writeLogEntry(LogTypeCmd, value, nil)
return err
}
if cmdStr == "monitor" {
// We need to turn on the monitor flag before we read the reply,
// otherwise some log msg might be treated as server reply
monitorMode.Store(true)
}
reply := <-serverReplyCh
if reply.err != nil {
return reply.err
}
h.writeLogEntry(LogTypeCmd, value, reply.value)
err = h.clientToSelfConn.WriteValue(reply.value, true)
if err != nil {
return err
}
if cmdStr == "monitor" && reply.value.Str != "OK" {
// looks like monitor cmd failed, let's revert the monitoring flag back
monitorMode.Store(false)
}
if cmdStr == "reset" && reply.value.Str == "OK" {
// the connection is reset, and we have exited monitor mode as well
monitorMode.Store(false)
// TODO: with reset cmd, should we send out AUTH again automatically to the server?
}
}
default:
if err = h.clientToSelfConn.WriteValue(&resp3.Value{Type: resp3.TypeSimpleError, Err: fmt.Sprintf("Unexpected value type %v", value.Type)}, true); err != nil {
return err
}
return fmt.Errorf("unexpected value type %v", value.Type)
}
}
}
func (r *RelayHandler) writeLogEntry(logType LogType, cmd *resp3.Value, resp *resp3.Value) {
entry := RedisLogEntry{
LogType: logType,
}
if logType == LogTypeCmd {
entry.Cmd = cmd.SmartResult()
}
input, err := valueToJson(entry)
if err != nil {
log.Error().Err(err).Msg("failed to convert cmd value to json")
return
}
output := ""
if resp != nil {
output, err = valueToJson(resp.SmartResult())
if err != nil {
log.Error().Err(err).Msg("failed to convert resp value to json")
return
}
}
err = r.sessionLogger.LogEntry(session.SessionLogEntry{
Timestamp: time.Now(),
Input: input,
Output: output,
})
if err != nil {
log.Error().Err(err).Msg("failed to write log entry to file")
}
}
func valueToJson(value interface{}) (string, error) {
data, err := json.Marshal(value)
if err != nil {
return "", err
}
return string(data), nil
}
func isPubSubConfirmation(value *resp3.Value) bool {
if len(value.Elems) < 1 {
return false
}
switch strings.ToLower(value.Elems[0].Str) {
case "subscribe", "psubscribe", "ssubscribe", "unsubscribe", "punsubscribe", "sunsubscribe":
return true
}
return false
}