Merge pull request #105008 from knight42/feat/kubectl-debug-general-profile

refactor(kubectl): add Debugger interface for kubectl-debug

Kubernetes-commit: 4702214d7840a3d00f0bf51195df90b1d47663bc
This commit is contained in:
Kubernetes Publisher 2022-06-02 13:26:20 -07:00
commit 7fa5b495e3
6 changed files with 196 additions and 36 deletions

4
go.mod
View File

@ -31,7 +31,7 @@ require (
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.0.0-20220531234821-832b1f4dc5da
k8s.io/apimachinery v0.0.0-20220527204257-be3a79b26814
k8s.io/cli-runtime v0.0.0-20220601000953-fc3349f2607f
k8s.io/cli-runtime v0.0.0-20220602191202-e90b85a3baf1
k8s.io/client-go v0.0.0-20220530124808-f88de91ae5c1
k8s.io/component-base v0.0.0-20220531235246-ba9c052508ce
k8s.io/component-helpers v0.0.0-20220509184522-76ad8716b7b9
@ -100,7 +100,7 @@ require (
replace (
k8s.io/api => k8s.io/api v0.0.0-20220531234821-832b1f4dc5da
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20220527204257-be3a79b26814
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20220601000953-fc3349f2607f
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20220602191202-e90b85a3baf1
k8s.io/client-go => k8s.io/client-go v0.0.0-20220530124808-f88de91ae5c1
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20220512215755-7d977b3e5454
k8s.io/component-base => k8s.io/component-base v0.0.0-20220531235246-ba9c052508ce

4
go.sum
View File

@ -763,8 +763,8 @@ k8s.io/api v0.0.0-20220531234821-832b1f4dc5da h1:lYt0O6S+waScMQNgNQSfPQOCtnjyL5w
k8s.io/api v0.0.0-20220531234821-832b1f4dc5da/go.mod h1:HcGtQRVUegP+lMiLiFI6ZTH8Gh2YLabQpXOnknalcVI=
k8s.io/apimachinery v0.0.0-20220527204257-be3a79b26814 h1:RvbQKYWzCfqGUeyc9Q4mR3TKLlzcygwItRsElVpf3BQ=
k8s.io/apimachinery v0.0.0-20220527204257-be3a79b26814/go.mod h1:1oBVxgNUfLl978lJAlywA+H45m2ctSuqJU2stpbcjT4=
k8s.io/cli-runtime v0.0.0-20220601000953-fc3349f2607f h1:dGwblOPNOxE4SGXoxRa3iVZZKaasGDC3RRn5UaM+4tY=
k8s.io/cli-runtime v0.0.0-20220601000953-fc3349f2607f/go.mod h1:rHkhDZPkVHhdbYYz2LzozHIJc3NRJmBa1Wczr2B1NoM=
k8s.io/cli-runtime v0.0.0-20220602191202-e90b85a3baf1 h1:XDkykuD460G5E3x5wGP6OzNlqVsv1iAIDRBwf8igkNw=
k8s.io/cli-runtime v0.0.0-20220602191202-e90b85a3baf1/go.mod h1:rHkhDZPkVHhdbYYz2LzozHIJc3NRJmBa1Wczr2B1NoM=
k8s.io/client-go v0.0.0-20220530124808-f88de91ae5c1 h1:BVUIe0zBQAxxmF44zVLf0exBG5g4sNHfgCIMJNnQyYw=
k8s.io/client-go v0.0.0-20220530124808-f88de91ae5c1/go.mod h1:aQ4aGSZhXBz/nxbhxf+vd3Y6JvHVWxkHHL5r2BD/DDo=
k8s.io/component-base v0.0.0-20220531235246-ba9c052508ce h1:F6Ee80Y4zrtMZrAV2Xcza/GnkLC9gtinQpgPzaCLSUo=

View File

@ -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
}

View File

@ -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,
},

View File

@ -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)
}

59
pkg/cmd/debug/profiles.go Normal file
View File

@ -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)
}
}