From fd0c15cce3d66bc2b32fc6943688425ec269d97d Mon Sep 17 00:00:00 2001 From: Jian Zeng Date: Fri, 12 Nov 2021 00:57:02 +0800 Subject: [PATCH] feat(kubectl): add debug profile applier Signed-off-by: Jian Zeng --- .../src/k8s.io/kubectl/pkg/cmd/debug/debug.go | 67 +++++++++++-------- .../kubectl/pkg/cmd/debug/debug_test.go | 49 ++++++++++++-- .../kubectl/pkg/cmd/debug/profile_applier.go | 49 ++++++++++++++ .../k8s.io/kubectl/pkg/cmd/debug/profiles.go | 59 ++++++++++++++++ 4 files changed, 192 insertions(+), 32 deletions(-) create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go index 0b830509bd4..046ea1d4cae 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -25,6 +25,7 @@ import ( "github.com/docker/distribution/reference" "github.com/spf13/cobra" "k8s.io/klog/v2" + "k8s.io/utils/pointer" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -52,7 +53,6 @@ import ( "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" - "k8s.io/utils/pointer" ) var ( @@ -122,6 +122,7 @@ type DebugOptions struct { ShareProcesses bool TargetContainer string TTY bool + Profile string attachChanged bool shareProcessedChanged bool @@ -130,6 +131,8 @@ type DebugOptions struct { genericclioptions.IOStreams warningPrinter *printers.WarningPrinter + + applier ProfileApplier } // NewDebugOptions returns a DebugOptions initialized with default values. @@ -179,6 +182,7 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) { cmd.Flags().BoolVar(&opt.ShareProcesses, "share-processes", opt.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy.")) cmd.Flags().StringVar(&opt.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name.")) cmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, i18n.T("Allocate a TTY for the debugging container.")) + cmd.Flags().StringVar(&opt.Profile, "profile", ProfileLegacy, i18n.T("Debugging profile.")) } // Complete finishes run-time initialization of debug.DebugOptions. @@ -222,6 +226,10 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st // Warning printer o.warningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) + o.applier, err = NewProfileApplier(o.Profile) + if err != nil { + return err + } return nil } @@ -383,7 +391,11 @@ func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error { // Returns an already created pod and container name for subsequent attach, if applicable. func (o *DebugOptions) visitNode(ctx context.Context, node *corev1.Node) (*corev1.Pod, string, error) { pods := o.podClient.Pods(o.Namespace) - newPod, err := pods.Create(ctx, o.generateNodeDebugPod(node), metav1.CreateOptions{}) + debugPod, err := o.generateNodeDebugPod(node) + if err != nil { + return nil, "", err + } + newPod, err := pods.Create(ctx, debugPod, metav1.CreateOptions{}) if err != nil { return nil, "", err } @@ -410,10 +422,12 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev return nil, "", fmt.Errorf("error creating JSON for pod: %v", err) } - debugContainer := o.generateDebugContainer(pod) + debugPod, debugContainer, err := o.generateDebugContainer(pod) + if err != nil { + return nil, "", err + } klog.V(2).Infof("new ephemeral container: %#v", debugContainer) - debugPod := pod.DeepCopy() - debugPod.Spec.EphemeralContainers = append(debugPod.Spec.EphemeralContainers, *debugContainer) + debugJS, err := json.Marshal(debugPod) if err != nil { return nil, "", fmt.Errorf("error creating JSON for debug container: %v", err) @@ -500,11 +514,10 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev return created, dc, nil } -// generateDebugContainer returns an EphemeralContainer suitable for use as a debug container +// generateDebugContainer returns a debugging pod and an EphemeralContainer suitable for use as a debug container // in the given pod. -func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) *corev1.EphemeralContainer { +func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *corev1.EphemeralContainer, error) { name := o.computeDebugContainerName(pod) - ec := &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: name, @@ -524,12 +537,18 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) *corev1.Ephemeral ec.Command = o.Args } - return ec + copied := pod.DeepCopy() + copied.Spec.EphemeralContainers = append(copied.Spec.EphemeralContainers, *ec) + if err := o.applier.Apply(copied, name, copied); err != nil { + return nil, nil, err + } + + return copied, ec, nil } // generateNodeDebugPod generates a debugging pod that schedules on the specified node. // The generated pod will run in the host PID, Network & IPC namespaces, and it will have the node's filesystem mounted at /host. -func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) *corev1.Pod { +func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, error) { cn := "debugger" // Setting a user-specified container name doesn't make much difference when there's only one container, // but the argument exists for pod debugging so it might be confusing if it didn't work here. @@ -559,27 +578,10 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) *corev1.Pod { Stdin: o.Interactive, TerminationMessagePolicy: corev1.TerminationMessageReadFile, TTY: o.TTY, - VolumeMounts: []corev1.VolumeMount{ - { - MountPath: "/host", - Name: "host-root", - }, - }, }, }, - HostIPC: true, - HostNetwork: true, - HostPID: true, NodeName: node.Name, RestartPolicy: corev1.RestartPolicyNever, - Volumes: []corev1.Volume{ - { - Name: "host-root", - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{Path: "/"}, - }, - }, - }, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, @@ -594,7 +596,11 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) *corev1.Pod { p.Spec.Containers[0].Command = o.Args } - return p + if err := o.applier.Apply(p, cn, node); err != nil { + return nil, err + } + + return p, nil } // generatePodCopyWithDebugContainer takes a Pod and returns a copy and the debug container name of that copy @@ -673,6 +679,11 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core c.Stdin = o.Interactive c.TTY = o.TTY + err := o.applier.Apply(copied, c.Name, pod) + if err != nil { + return nil, "", err + } + return copied, name, nil } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go index 40f88f7709c..bd4f8bb974c 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go @@ -18,18 +18,20 @@ package debug import ( "fmt" - "github.com/spf13/cobra" "strings" "testing" "time" + "github.com/spf13/cobra" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/utils/pointer" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/utils/pointer" ) func TestGenerateDebugContainer(t *testing.T) { @@ -233,7 +235,18 @@ func TestGenerateDebugContainer(t *testing.T) { if tc.pod == nil { tc.pod = &corev1.Pod{} } - if diff := cmp.Diff(tc.expected, tc.opts.generateDebugContainer(tc.pod)); diff != "" { + + applier, err := NewProfileApplier(ProfileLegacy) + if err != nil { + t.Fatalf("fail to create %s profile", ProfileLegacy) + } + tc.opts.applier = applier + + _, debugContainer, err := tc.opts.generateDebugContainer(tc.pod) + if err != nil { + t.Fatalf("fail to generate debug container: %v", err) + } + if diff := cmp.Diff(tc.expected, debugContainer); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) @@ -1003,6 +1016,11 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + var err error + tc.opts.applier, err = NewProfileApplier(ProfileLegacy) + if err != nil { + t.Fatalf("Fail to create legacy profile: %v", err) + } tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() suffixCounter = 0 @@ -1193,10 +1211,18 @@ func TestGenerateNodeDebugPod(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + var err error + tc.opts.applier, err = NewProfileApplier(ProfileLegacy) + if err != nil { + t.Fatalf("Fail to create legacy profile: %v", err) + } tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() suffixCounter = 0 - pod := tc.opts.generateNodeDebugPod(tc.node) + pod, err := tc.opts.generateNodeDebugPod(tc.node) + if err != nil { + t.Fatalf("Fail to generate node debug pod: %v", err) + } if diff := cmp.Diff(tc.expected, pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } @@ -1255,6 +1281,7 @@ func TestCompleteAndValidate(t *testing.T) { Namespace: "test", PullPolicy: corev1.PullPolicy("Always"), ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, @@ -1266,6 +1293,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "busybox", Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod1", "mypod2"}, }, }, @@ -1277,6 +1305,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "busybox", Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod1", "mypod2"}, }, }, @@ -1290,6 +1319,7 @@ func TestCompleteAndValidate(t *testing.T) { Interactive: true, Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, @@ -1303,6 +1333,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "busybox", Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, @@ -1316,6 +1347,7 @@ func TestCompleteAndValidate(t *testing.T) { Interactive: true, Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, @@ -1329,6 +1361,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "myproj/debug-tools", Namespace: "test", PullPolicy: corev1.PullPolicy("Always"), + Profile: ProfileLegacy, ShareProcesses: true, TargetNames: []string{"mypod"}, }, @@ -1369,6 +1402,7 @@ func TestCompleteAndValidate(t *testing.T) { Interactive: true, Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, @@ -1383,6 +1417,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "busybox", Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, @@ -1396,6 +1431,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "busybox", Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, @@ -1409,6 +1445,7 @@ func TestCompleteAndValidate(t *testing.T) { Image: "busybox", Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, @@ -1424,6 +1461,7 @@ func TestCompleteAndValidate(t *testing.T) { "app": "app-debugger", }, ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, @@ -1442,6 +1480,7 @@ func TestCompleteAndValidate(t *testing.T) { "sidecar": "sidecar:debug", }, ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, @@ -1457,6 +1496,7 @@ func TestCompleteAndValidate(t *testing.T) { Interactive: true, Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, @@ -1496,6 +1536,7 @@ func TestCompleteAndValidate(t *testing.T) { Interactive: true, Namespace: "test", ShareProcesses: true, + Profile: ProfileLegacy, TargetNames: []string{"node/mynode"}, TTY: true, }, diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go new file mode 100644 index 00000000000..d2a71a23aa4 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 The Kubernetes Authors. + +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 debug + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ProfileLegacy represents the legacy debugging profile which is backwards-compatible with 1.23 behavior. +const ProfileLegacy = "legacy" + +type ProfileApplier interface { + // Apply applies the profile to the given container in the pod. + Apply(pod *corev1.Pod, containerName string, target runtime.Object) error +} + +// NewProfileApplier returns a new Options for the given profile name. +func NewProfileApplier(profile string) (ProfileApplier, error) { + switch profile { + case ProfileLegacy: + return applierFunc(profileLegacy), nil + } + + return nil, fmt.Errorf("unknown profile: %s", profile) +} + +// applierFunc is a function that applies a profile to a container in the pod. +type applierFunc func(pod *corev1.Pod, containerName string, target runtime.Object) error + +func (f applierFunc) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { + return f(pod, containerName, target) +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go new file mode 100644 index 00000000000..07b14a6d6c1 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go @@ -0,0 +1,59 @@ +/* +Copyright 2022 The Kubernetes Authors. + +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 debug + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// profileLegacy represents the legacy debugging profile which is backwards-compatible with 1.23 behavior. +func profileLegacy(pod *corev1.Pod, containerName string, target runtime.Object) error { + switch target.(type) { + case *corev1.Pod: + // do nothing to the copied pod + return nil + case *corev1.Node: + const volumeName = "host-root" + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: "/"}, + }, + }) + + for i := range pod.Spec.Containers { + container := &pod.Spec.Containers[i] + if container.Name != containerName { + continue + } + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + MountPath: "/host", + Name: volumeName, + }) + } + + pod.Spec.HostIPC = true + pod.Spec.HostNetwork = true + pod.Spec.HostPID = true + return nil + default: + return fmt.Errorf("the %s profile doesn't support objects of type %T", ProfileLegacy, target) + } +}