mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
feat(kubectl): add debug profile applier
Signed-off-by: Jian Zeng <anonymousknight96@gmail.com>
This commit is contained in:
parent
f9f9e7177a
commit
fd0c15cce3
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
|
49
staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go
Normal file
49
staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go
Normal 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
staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go
Normal file
59
staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user