feat(kubectl): add debug profile applier

Signed-off-by: Jian Zeng <anonymousknight96@gmail.com>
This commit is contained in:
Jian Zeng 2021-11-12 00:57:02 +08:00 committed by Jian Zeng
parent f9f9e7177a
commit fd0c15cce3
No known key found for this signature in database
GPG Key ID: 1040B69865E7D86C
4 changed files with 192 additions and 32 deletions

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

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