Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/kubectl-ate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ By default, `kubectl-ate` will automatically read your `~/.kube/config`, discove

If you prefer to route traffic directly (e.g., through a LoadBalancer or when running natively inside a cluster pod), simply provide the `--endpoint` flag to bypass the tunnel.

Because the tunnel is an implementation detail, diagnostics that the Kubernetes client reports about it are not printed. Pass `--verbose` to see them when troubleshooting a connection.

## Tracing
The CLI supports on-demand tracing using the `--trace` flag. When enabled, the CLI will generate a trace ID and signal to the server that it wants the request to be traced.

Expand Down Expand Up @@ -67,6 +69,7 @@ These flags can be appended to any command:
| `--endpoint` | | Manual gRPC endpoint override (e.g., `localhost:8080`) | |
| `--output` | `-o` | Output format (`table`, `json`, `yaml`) | `table` |
| `--trace` | | Enable on-demand tracing for the request | `false` |
| `--verbose` | | Print internal Kubernetes client diagnostics that are otherwise suppressed | `false` |

---

Expand Down
51 changes: 51 additions & 0 deletions cmd/kubectl-ate/internal/cmd/clientlogging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"fmt"
"io"

utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)

// configureClientLogging keeps client-go's unhandled-error reports out of the
// user's terminal unless --verbose asks for them.
//
// client-go reports problems it cannot return to a caller through
// utilruntime.HandleError, whose default handler writes to stderr naming the
// client-go file that failed, for example:
//
// E0520 15:40:33.154830 3507355 portforward.go:502] "Unhandled Error" err="..."
//
// Someone running `kubectl ate create actor` cannot act on that, and the
// port-forwarding it refers to is an implementation detail of this plugin.
//
// klog is deliberately left alone. client-go logs warnings a user can act on
// directly through it, such as a missing kubeconfig or a credential plugin that
// failed to refresh, and those should keep reaching the terminal. Replacing
// ErrorHandlers also drops upstream's 1ms rate limiter, which existed only
// because the default handler writes to stderr.
func configureClientLogging(errOut io.Writer, verbose bool) {
utilruntime.ErrorHandlers = []utilruntime.ErrorHandler{
func(_ context.Context, err error, msg string, keysAndValues ...any) {
if !verbose {
return
}
fmt.Fprintf(errOut, "kubectl-ate: %s\n", utilruntime.ErrorToString(err, msg, keysAndValues...))
},
}
}
88 changes: 88 additions & 0 deletions cmd/kubectl-ate/internal/cmd/clientlogging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"bytes"
"errors"
"strings"
"testing"

utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)

// errPortForwardReset reproduces the client-go report from
// https://github.com/agent-substrate/substrate/issues/26, which surfaced after a
// successful command.
var errPortForwardReset = errors.New("error copying from local connection to remote stream: " +
"read tcp4 127.0.0.1:42521->127.0.0.1:42648: read: connection reset by peer")

func restoreClientLogging(t *testing.T) {
t.Helper()
handlers := utilruntime.ErrorHandlers
t.Cleanup(func() { utilruntime.ErrorHandlers = handlers })
}

func TestConfigureClientLogging(t *testing.T) {
tests := []struct {
name string
verbose bool
want string
}{
{name: "quiet by default"},
{name: "verbose", verbose: true, want: "connection reset by peer"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restoreClientLogging(t)

var errOut bytes.Buffer
configureClientLogging(&errOut, test.verbose)
utilruntime.HandleError(errPortForwardReset)

got := errOut.String()
if test.want == "" {
if got != "" {
t.Errorf("stderr = %q, want no output", got)
}
return
}
if !strings.Contains(got, test.want) {
t.Errorf("stderr = %q, want it to contain %q", got, test.want)
}
})
}
}

func TestRootCommandConfiguresClientLogging(t *testing.T) {
restoreClientLogging(t)

verbose = false
t.Cleanup(func() { verbose = false })

var errOut bytes.Buffer
rootCmd.SetErr(&errOut)
t.Cleanup(func() { rootCmd.SetErr(nil) })

if err := rootCmd.PersistentPreRunE(rootCmd, nil); err != nil {
t.Fatalf("PersistentPreRunE: %v", err)
}
utilruntime.HandleError(errPortForwardReset)

if errOut.Len() != 0 {
t.Errorf("stderr = %q, want no output", errOut.String())
}
}
3 changes: 3 additions & 0 deletions cmd/kubectl-ate/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
endpoint string
outputFmt string
traceEnabled bool
verbose bool
)

var rootCmd = &cobra.Command{
Expand All @@ -41,6 +42,7 @@ var rootCmd = &cobra.Command{
if outputFmt != "table" && outputFmt != "json" && outputFmt != "yaml" {
return fmt.Errorf("invalid output format %q. Must be one of: table, json, yaml", outputFmt)
}
configureClientLogging(cmd.ErrOrStderr(), verbose)
return nil
},
}
Expand All @@ -58,4 +60,5 @@ func init() {
rootCmd.PersistentFlags().StringVar(&endpoint, "endpoint", "", "Manual override for the gRPC target (e.g., localhost:8080). If omitted, automatically port-forwards.")
rootCmd.PersistentFlags().StringVarP(&outputFmt, "output", "o", "table", "Output format. One of: table|json|yaml")
rootCmd.PersistentFlags().BoolVar(&traceEnabled, "trace", false, "Enable tracing for the request")
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Print internal Kubernetes client diagnostics that are otherwise suppressed")
}