@@ -22,16 +22,20 @@ import (
2222 "fmt"
2323 "io"
2424 "os"
25+ "os/signal"
2526 "regexp"
27+ "syscall"
2628 "time"
2729
2830 "github.com/spf13/cobra"
2931 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3032 "k8s.io/cli-runtime/pkg/genericclioptions"
33+ "k8s.io/cli-runtime/pkg/genericiooptions"
3134 "k8s.io/client-go/kubernetes"
3235 cliflag "k8s.io/component-base/cli/flag"
3336 "k8s.io/component-base/logs"
3437 kubecmd "k8s.io/kubectl/pkg/cmd"
38+ "k8s.io/kubectl/pkg/cmd/plugin"
3539)
3640
3741const (
@@ -42,6 +46,33 @@ const (
4246
4347var d8CommandRegex = regexp .MustCompile ("([\" '`])d8 (\\ w+)" )
4448
49+ // d8KubectlWriter wraps an io.Writer and rewrites kubectl's "d8 <subcmd>"
50+ // references (matched by d8CommandRegex) to "d8 k <subcmd>" on each Write call.
51+ //
52+ // kubectl uses os.Args[0] as the displayed command name in user-facing
53+ // suggestions (e.g. "You can run `d8 replace -f ...` to try this update again.").
54+ // Since the binary is invoked as "d8", those suggestions point users to a
55+ // non-existent top-level command. Wrapping IOStreams.ErrOut with this writer
56+ // ensures the suggestions are rewritten to the correct "d8 k <subcmd>" form
57+ // before reaching the terminal.
58+ type d8KubectlWriter struct {
59+ w io.Writer
60+ }
61+
62+ func newD8KubectlWriter (w io.Writer ) * d8KubectlWriter {
63+ return & d8KubectlWriter {w : w }
64+ }
65+
66+ func (d * d8KubectlWriter ) Write (p []byte ) (int , error ) {
67+ rewritten := d8CommandRegex .ReplaceAllString (string (p ), "${1}d8 k ${2}" )
68+ if _ , err := d .w .Write ([]byte (rewritten )); err != nil {
69+ return 0 , err
70+ }
71+ // Report the original input length to honor the io.Writer contract even
72+ // though the rewritten payload may have a different byte length.
73+ return len (p ), nil
74+ }
75+
4576// wrapRunE wraps all RunE functions in the kubectl command tree to intercept stderr output.
4677// This approach is preferred over modifying os.Args[0] because:
4778// - It avoids modifying global state (os.Args) which could affect other parts of the system
@@ -126,12 +157,37 @@ func getDebugImage(cmd *cobra.Command) (string, error) {
126157}
127158
128159func NewKubectlCommand () * cobra.Command {
129- kubectlCmd := kubecmd .NewDefaultKubectlCommand ()
160+ // Build a kubectl command tree with stderr wrapped by d8KubectlWriter so
161+ // kubectl's "d8 <subcmd>" command hints are rewritten to "d8 k <subcmd>".
162+ //
163+ // This must be applied at construction time: kubectl captures os.Stderr
164+ // once into IOStreams.ErrOut and stores the reference (see
165+ // k8s.io/kubectl/pkg/cmd/cmd.go:NewDefaultKubectlCommand). Code paths that
166+ // write via that stored reference (e.g. the post-edit hint
167+ // "You can run `d8 replace -f ...`" emitted from editoptions.go) cannot be
168+ // intercepted by later swaps of the os.Stderr global.
169+ ioStreams := genericiooptions.IOStreams {
170+ In : os .Stdin ,
171+ Out : os .Stdout ,
172+ ErrOut : newD8KubectlWriter (os .Stderr ),
173+ }
174+
175+ kubectlCmd := kubecmd .NewDefaultKubectlCommandWithArgs (kubecmd.KubectlOptions {
176+ PluginHandler : kubecmd .NewDefaultPluginHandler (plugin .ValidPluginFilenamePrefixes ),
177+ Arguments : os .Args ,
178+ ConfigFlags : genericclioptions .NewConfigFlags (true ).
179+ WithDeprecatedPasswordFlag ().
180+ WithDiscoveryBurst (300 ).
181+ WithDiscoveryQPS (50.0 ).
182+ WithWarningPrinter (ioStreams ),
183+ IOStreams : ioStreams ,
184+ })
130185 kubectlCmd .Use = "k"
131186 kubectlCmd .Aliases = []string {"kubectl" }
132187 kubectlCmd = ReplaceCommandName ("kubectl" , "d8 k" , kubectlCmd )
133188
134- // Wrap RunE to fix error messages
189+ // Fallback rewrite for kubectl code paths that write to the global
190+ // os.Stderr directly instead of using IOStreams.ErrOut.
135191 wrapRunE (kubectlCmd )
136192
137193 var debugCmd * cobra.Command
@@ -150,6 +206,22 @@ func NewKubectlCommand() *cobra.Command {
150206
151207 originalPersistentPreRunE := kubectlCmd .PersistentPreRunE
152208 kubectlCmd .PersistentPreRunE = func (cmd * cobra.Command , args []string ) error {
209+ // Restore default OS signal handling for the kubectl subtree.
210+ //
211+ // The d8 root command installs a graceful-termination signal handler
212+ // (see graceful.WithTermination in NewDeliveryCommand) that intercepts
213+ // SIGINT/SIGTERM, cancels the root context and then resets the signal
214+ // handlers. The kubectl subcommands (notably long-running ones such as
215+ // `proxy`, `port-forward`, `exec`, `attach`, `logs -f`) do not observe
216+ // that context and keep running until a second signal arrives and is
217+ // delivered to the default Go handler.
218+ //
219+ // To match standalone kubectl behavior (single SIGINT/SIGTERM stops the
220+ // command), drop our signal interceptor before kubectl starts so the
221+ // very first signal is delivered to the default handler and terminates
222+ // the process immediately.
223+ signal .Reset (syscall .SIGINT , syscall .SIGTERM )
224+
153225 if cmd .Name () == "debug" || (cmd .Parent () != nil && cmd .Parent ().Name () == "debug" ) {
154226 imageFlag := cmd .Flags ().Lookup ("image" )
155227 if imageFlag != nil && imageFlag .Value .String () == "" {
0 commit comments