diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d564802 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mcgod diff --git a/cmd/mcgod/main.go b/cmd/mcgod/main.go index 8c47a5e..021682f 100644 --- a/cmd/mcgod/main.go +++ b/cmd/mcgod/main.go @@ -2,25 +2,70 @@ package main import ( "context" + "fmt" "log" + "log/slog" "os" "os/signal" + "sync" "syscall" + "time" + "github.com/gogo/protobuf/proto" + "github.com/ollama/ollama/api" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "tipsy.codes/charles/mc-god/v2/internal/pkg/logs" "tipsy.codes/charles/mc-god/v2/internal/pkg/rcon" ) +type chatContext struct { + chatRequest *api.ChatRequest + totalSize int + maxSize int + mu sync.Mutex +} + +func (c *chatContext) AddLog(msg string) { + c.mu.Lock() + defer c.mu.Unlock() + c.chatRequest.Messages = append(c.chatRequest.Messages, api.Message{ + Role: "logs", + Content: msg, + }) + c.totalSize += len(msg) + c.truncate() +} + +func (c *chatContext) AddSelf(msg api.Message) { + c.mu.Lock() + defer c.mu.Unlock() + c.chatRequest.Messages = append(c.chatRequest.Messages, msg) + c.totalSize += len(msg.Content) + c.truncate() +} + +func (c *chatContext) truncate() { + for c.maxSize != 0 && c.totalSize > c.maxSize && len(c.chatRequest.Messages) > 0 { + t := c.chatRequest.Messages[0] + c.chatRequest.Messages = c.chatRequest.Messages[1:] + c.totalSize -= len(t.Content) + } +} + func main() { // Create a context that will be cancelled on interrupt signals ctx, cancel := context.WithCancel(context.Background()) defer cancel() + _ = ctx // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan - log.Println("Received interrupt signal, shutting down...") + slog.Info("received interrupt signal, shutting down...") cancel() }() @@ -28,18 +73,142 @@ func main() { log.Println("Connecting to Minecraft server via RCON...") client, err := rcon.NewFromEnv() if err != nil { - log.Fatalf("Failed to create RCON client: %v", err) + slog.Error("failed to create RCON client", "error", err) + return } defer func() { if err := client.Close(); err != nil { - log.Printf("Error closing RCON connection: %v", err) + slog.Warn("error closing RCON connection", "error", err) } }() // Perform a health check - log.Println("Performing health check...") + log.Println("Performing healthcheck...") if err := client.HealthCheck(); err != nil { - log.Fatalf("Health check failed: %v", err) + slog.Error("Health check failed", "error", err) + return } log.Println("Connected successfully!") + + // Create Kubernetes client + kClient, err := createKubernetesClient() + if err != nil { + slog.Error("failed to create kubernetes client", "error", err) + return + } + slog.Info("got kubernetes config") + + tailer, done := logs.LoggerFromEnv().Start(ctx, kClient) + defer func() { + if err := done(); err != nil { + slog.Error("problem with tailer", "error", err) + } + }() + slog.Info("logger started") + + ollamaClient, err := api.ClientFromEnvironment() + if err != nil { + slog.Error("error getting ollama client", "error", err) + } + + rClient, err := rcon.NewFromEnv() + if err != nil { + slog.Error("failed to get rcon client", "error", err) + return + } + + // Start goroutines to do the things + chatRequest := &api.ChatRequest{ + Model: "qwen3-coder", + Stream: proto.Bool(false), + KeepAlive: &api.Duration{Duration: time.Hour}, + Tools: api.Tools{}, + Think: &api.ThinkValue{Value: false}, + Shift: proto.Bool(true), + Messages: []api.Message{ + api.Message{ + Role: "system", + Content: ` + You are Minecraft server admin with a god complex. + React to any messages from the user by saying something god-like. + When you join the server, announce yourself. + + Responses should be short; one sentence. + You may choose to return an empty response if there is nothing interesting to say + (i.e., no new logs since your last message). + + When a user replies to you, you will see this in the logs: + + 2026/02/14 10:48:40 INFO mc log msg="[18:45:10] [Server thread/INFO]: you are full of it." + + The user here is OrangeYouSad, who said "you are full of it." + `, + }, + }, + } + chat := &chatContext{ + chatRequest: chatRequest, + maxSize: 10000000, + } + + doneWg := sync.WaitGroup{} + doneWg.Go(handleOllama(ctx, ollamaClient, chat, rClient)) + + for line := range tailer.NextLine() { + slog.Info("mc log", "msg", line) + chat.AddLog(line) + } + + doneWg.Wait() +} + +func handleOllama(ctx context.Context, client *api.Client, chat *chatContext, rClient *rcon.Client) func() { + slog.Info("got chat request", "object", fmt.Sprintf("%+v", chat.chatRequest)) + return func() { + var chatResponse api.ChatResponse + for { + select { + case <-ctx.Done(): + return + case <-time.Tick(time.Second * 10): + // do nothing + } + chat.mu.Lock() + // slog.Info("sending chat request", "object", fmt.Sprintf("%#v", chat.chatRequest)) + err := client.Chat(ctx, chat.chatRequest, func(cr api.ChatResponse) error { + chatResponse = cr + return nil + }) + chat.mu.Unlock() + if err != nil { + slog.Error("error calling ollama", "error", err) + } + chat.AddSelf(chatResponse.Message) + if err := rClient.Say(chatResponse.Message.Content); err != nil { + slog.Error("error talking", "error", err) + } + } + } +} + +func createKubernetesClient() (*kubernetes.Clientset, error) { + // Try to load in-cluster config first + config, err := rest.InClusterConfig() + if err != nil { + // If in-cluster config fails, try kubeconfig + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + config, err = kubeConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes client: %w", err) + } + } + + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes client: %w", err) + } + + return client, nil } diff --git a/go.mod b/go.mod index 4787b05..c78f0f6 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,19 @@ module tipsy.codes/charles/mc-god/v2 go 1.25.6 require ( + github.com/gogo/protobuf v1.3.2 github.com/google/go-cmp v0.7.0 github.com/gorcon/rcon v1.4.0 - github.com/stretchr/testify v1.9.0 + github.com/ollama/ollama v0.16.1 + github.com/stretchr/testify v1.11.1 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -19,7 +23,6 @@ require ( github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -33,12 +36,14 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index dec885e..461db14 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -60,6 +64,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ollama/ollama v0.16.1 h1:DIxnLdS0om3hb7HheJqj6+ZnPCCMWmy/vyUxiQgRYoI= +github.com/ollama/ollama v0.16.1/go.mod h1:FEk95NbAJJZk+t7cLh+bPGTul72j1O3PLLlYNV3FVZ0= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= @@ -78,8 +84,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -87,14 +95,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -103,22 +113,22 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/pkg/logs/logs.go b/internal/pkg/logs/logs.go index 7f8fd13..90d8f67 100644 --- a/internal/pkg/logs/logs.go +++ b/internal/pkg/logs/logs.go @@ -4,13 +4,12 @@ import ( "bufio" "context" "fmt" + "os" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) // Logger represents a Kubernetes log tailer @@ -30,6 +29,20 @@ type Logger struct { Timeout time.Duration } +func LoggerFromEnv() *Logger { + deployment := os.Getenv("RCON_DEPLOYMENT") + namespace := os.Getenv("RCON_NAMESPACE") + pod := os.Getenv("RCON_POD") + container := os.Getenv("RCON_CONTAINER") + + return &Logger{ + Deployment: deployment, + Namespace: namespace, + Pod: pod, + Container: container, + } +} + // Tailer represents a running log tailer type Tailer struct { lines chan string @@ -47,23 +60,17 @@ func (t *Tailer) Stop() { } // Start starts the log tailer -func (l *Logger) Start() (*Tailer, func() error) { +func (l *Logger) Start(ctx context.Context, client *kubernetes.Clientset) (*Tailer, func() error) { tailer := &Tailer{ lines: make(chan string), done: make(chan struct{}), } - // Create Kubernetes client - client, err := l.createKubernetesClient() - if err != nil { - close(tailer.lines) - return tailer, func() error { return err } - } - // Get pod name if not specified + var err error podName := l.Pod if podName == "" { - podName, err = l.getPodName(client) + podName, err = l.getPodName(ctx, client) if err != nil { close(tailer.lines) return tailer, func() error { return err } @@ -73,7 +80,7 @@ func (l *Logger) Start() (*Tailer, func() error) { // Get container name if not specified containerName := l.Container if containerName == "" { - containerName, err = l.getContainerName(client, podName) + containerName, err = l.getContainerName(ctx, client, podName) if err != nil { close(tailer.lines) return tailer, func() error { return err } @@ -81,7 +88,7 @@ func (l *Logger) Start() (*Tailer, func() error) { } // Start tailing logs - go l.tailLogs(client, podName, containerName, tailer) + go l.tailLogs(ctx, client, podName, containerName, tailer) return tailer, func() error { tailer.Stop() @@ -89,31 +96,9 @@ func (l *Logger) Start() (*Tailer, func() error) { } } -func (l *Logger) createKubernetesClient() (*kubernetes.Clientset, error) { - // Try to load in-cluster config first - config, err := rest.InClusterConfig() - if err != nil { - // If in-cluster config fails, try kubeconfig - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - configOverrides := &clientcmd.ConfigOverrides{} - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) - config, err = kubeConfig.ClientConfig() - if err != nil { - return nil, fmt.Errorf("failed to create kubernetes client: %w", err) - } - } - - client, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create kubernetes client: %w", err) - } - - return client, nil -} - -func (l *Logger) getPodName(client *kubernetes.Clientset) (string, error) { +func (l *Logger) getPodName(ctx context.Context, client *kubernetes.Clientset) (string, error) { // List pods with the deployment label - podList, err := client.CoreV1().Pods(l.Namespace).List(context.TODO(), metav1.ListOptions{ + podList, err := client.CoreV1().Pods(l.Namespace).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("app=%s", l.Deployment), }) if err != nil { @@ -135,8 +120,8 @@ func (l *Logger) getPodName(client *kubernetes.Clientset) (string, error) { return podList.Items[0].Name, nil } -func (l *Logger) getContainerName(client *kubernetes.Clientset, podName string) (string, error) { - pod, err := client.CoreV1().Pods(l.Namespace).Get(context.TODO(), podName, metav1.GetOptions{}) +func (l *Logger) getContainerName(ctx context.Context, client *kubernetes.Clientset, podName string) (string, error) { + pod, err := client.CoreV1().Pods(l.Namespace).Get(ctx, podName, metav1.GetOptions{}) if err != nil { return "", fmt.Errorf("failed to get pod %s: %w", podName, err) } @@ -149,7 +134,7 @@ func (l *Logger) getContainerName(client *kubernetes.Clientset, podName string) return pod.Spec.Containers[0].Name, nil } -func (l *Logger) tailLogs(client *kubernetes.Clientset, podName, containerName string, tailer *Tailer) { +func (l *Logger) tailLogs(ctx context.Context, client *kubernetes.Clientset, podName, containerName string, tailer *Tailer) { defer close(tailer.lines) // Prepare log request @@ -163,7 +148,6 @@ func (l *Logger) tailLogs(client *kubernetes.Clientset, podName, containerName s } // Create context with timeout if specified - ctx := context.Background() if l.Timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, l.Timeout) diff --git a/internal/pkg/rcon/rcon.go b/internal/pkg/rcon/rcon.go index ecf7f84..5aae31d 100644 --- a/internal/pkg/rcon/rcon.go +++ b/internal/pkg/rcon/rcon.go @@ -69,6 +69,11 @@ func (c *Client) SetDifficulty(difficulty string) error { return fmt.Errorf("not implemented") } +func (c *Client) Say(msg string) error { + _, err := c.Execute("/say " + msg) + return err +} + // GetServerInfo returns basic server information func (c *Client) GetServerInfo() (string, error) { return c.Execute("version")