diff --git a/pkg/cmd/debug/debug.go b/pkg/cmd/debug/debug.go index 7a6bea2f..53ca2660 100644 --- a/pkg/cmd/debug/debug.go +++ b/pkg/cmd/debug/debug.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "os" "time" "github.com/distribution/reference" @@ -106,29 +107,33 @@ var ( var nameSuffixFunc = utilrand.String +type DebugAttachFunc func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error + // DebugOptions holds the options for an invocation of kubectl debug. type DebugOptions struct { - Args []string - ArgsOnly bool - Attach bool - AttachFunc func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error - Container string - CopyTo string - Replace bool - Env []corev1.EnvVar - Image string - Interactive bool - Namespace string - TargetNames []string - PullPolicy corev1.PullPolicy - Quiet bool - SameNode bool - SetImages map[string]string - ShareProcesses bool - TargetContainer string - TTY bool - Profile string - Applier ProfileApplier + Args []string + ArgsOnly bool + Attach bool + AttachFunc DebugAttachFunc + Container string + CopyTo string + Replace bool + Env []corev1.EnvVar + Image string + Interactive bool + Namespace string + TargetNames []string + PullPolicy corev1.PullPolicy + Quiet bool + SameNode bool + SetImages map[string]string + ShareProcesses bool + TargetContainer string + TTY bool + Profile string + CustomProfileFile string + CustomProfile *corev1.Container + Applier ProfileApplier explicitNamespace bool attachChanged bool @@ -193,6 +198,9 @@ func (o *DebugOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name.")) cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, i18n.T("Allocate a TTY for the debugging container.")) cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Options are "legacy", "general", "baseline", "netadmin", "restricted" or "sysadmin".`)) + if cmdutil.DebugCustomProfile.IsEnabled() { + cmd.Flags().StringVar(&o.CustomProfileFile, "custom", o.CustomProfileFile, i18n.T("Path to a JSON file containing a partial container spec to customize built-in debug profiles.")) + } } // Complete finishes run-time initialization of debug.DebugOptions. @@ -256,6 +264,18 @@ func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGet o.Applier = applier } + if o.CustomProfileFile != "" { + customProfileBytes, err := os.ReadFile(o.CustomProfileFile) + if err != nil { + return fmt.Errorf("must pass a container spec json file for custom profile: %w", err) + } + + err = json.Unmarshal(customProfileBytes, &o.CustomProfile) + if err != nil { + return fmt.Errorf("%s does not contain a valid container spec: %w", o.CustomProfileFile, err) + } + } + clientConfig, err := restClientGetter.ToRESTConfig() if err != nil { return err @@ -348,6 +368,12 @@ func (o *DebugOptions) Validate() error { return fmt.Errorf("WarningPrinter can not be used without initialization") } + if o.CustomProfile != nil { + if o.CustomProfile.Name != "" || len(o.CustomProfile.Command) > 0 || o.CustomProfile.Image != "" || o.CustomProfile.Lifecycle != nil || len(o.CustomProfile.VolumeDevices) > 0 { + return fmt.Errorf("name, command, image, lifecycle and volume devices are not modifiable via custom profile") + } + } + return nil } @@ -467,6 +493,90 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev return result, debugContainer.Name, nil } +// applyCustomProfile applies given partial container json file on to the profile +// incorporated debug pod. +func (o *DebugOptions) applyCustomProfile(debugPod *corev1.Pod, containerName string) error { + o.CustomProfile.Name = containerName + customJS, err := json.Marshal(o.CustomProfile) + if err != nil { + return fmt.Errorf("unable to marshall custom profile: %w", err) + } + + var index int + found := false + for i, val := range debugPod.Spec.Containers { + if val.Name == containerName { + index = i + found = true + break + } + } + + if !found { + return fmt.Errorf("unable to find the %s container in the pod %s", containerName, debugPod.Name) + } + + var debugContainerJS []byte + debugContainerJS, err = json.Marshal(debugPod.Spec.Containers[index]) + if err != nil { + return fmt.Errorf("unable to marshall container: %w", err) + } + + patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{}) + if err != nil { + return fmt.Errorf("error creating three way patch to add debug container: %w", err) + } + + err = json.Unmarshal(patchedContainer, &debugPod.Spec.Containers[index]) + if err != nil { + return fmt.Errorf("unable to unmarshall patched container to container: %w", err) + } + + return nil +} + +// applyCustomProfileEphemeral applies given partial container json file on to the profile +// incorporated ephemeral container of the pod. +func (o *DebugOptions) applyCustomProfileEphemeral(debugPod *corev1.Pod, containerName string) error { + o.CustomProfile.Name = containerName + customJS, err := json.Marshal(o.CustomProfile) + if err != nil { + return fmt.Errorf("unable to marshall custom profile: %w", err) + } + + var index int + found := false + for i, val := range debugPod.Spec.EphemeralContainers { + if val.Name == containerName { + index = i + found = true + break + } + } + + if !found { + return fmt.Errorf("unable to find the %s ephemeral container in the pod %s", containerName, debugPod.Name) + } + + var debugContainerJS []byte + debugContainerJS, err = json.Marshal(debugPod.Spec.EphemeralContainers[index]) + if err != nil { + return fmt.Errorf("unable to marshall ephemeral container:%w", err) + } + + patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{}) + if err != nil { + return fmt.Errorf("error creating three way patch to add debug container: %w", err) + } + + err = json.Unmarshal(patchedContainer, &debugPod.Spec.EphemeralContainers[index]) + if err != nil { + return fmt.Errorf("unable to unmarshall patched container to ephemeral container: %w", err) + } + + return nil +} + // debugByCopy runs a copy of the target Pod with a debug container added or an original container modified func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { copied, dc, err := o.generatePodCopyWithDebugContainer(pod) @@ -515,6 +625,13 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *co return nil, nil, err } + if o.CustomProfile != nil { + err := o.applyCustomProfileEphemeral(copied, ec.Name) + if err != nil { + return nil, nil, err + } + } + ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1] return copied, ec, nil @@ -574,6 +691,13 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, err return nil, err } + if o.CustomProfile != nil { + err := o.applyCustomProfile(p, cn) + if err != nil { + return nil, err + } + } + return p, nil } @@ -656,6 +780,13 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core return nil, "", err } + if o.CustomProfile != nil { + err = o.applyCustomProfile(copied, name) + if err != nil { + return nil, "", err + } + } + return copied, name, nil } diff --git a/pkg/cmd/debug/debug_test.go b/pkg/cmd/debug/debug_test.go index 035e7f6d..a4c25606 100644 --- a/pkg/cmd/debug/debug_test.go +++ b/pkg/cmd/debug/debug_test.go @@ -22,6 +22,8 @@ import ( "testing" "time" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/cobra" @@ -1755,6 +1757,689 @@ func TestGenerateNodeDebugPod(t *testing.T) { } } +func TestGenerateNodeDebugPodCustomProfile(t *testing.T) { + for _, tc := range []struct { + name string + node *corev1.Node + opts *DebugOptions + expected *corev1.Pod + }{ + { + name: "baseline profile", + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-XXX", + }, + }, + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileBaseline, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-debugger-node-XXX-1", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + NodeName: "node-XXX", + RestartPolicy: corev1.RestartPolicyNever, + Volumes: nil, + Tolerations: []corev1.Toleration{ + { + Operator: corev1.TolerationOpExists, + }, + }, + }, + }, + }, + { + name: "restricted profile", + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-XXX", + }, + }, + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileRestricted, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-debugger-node-XXX-1", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + AllowPrivilegeEscalation: pointer.Bool(false), + SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + NodeName: "node-XXX", + RestartPolicy: corev1.RestartPolicyNever, + Volumes: nil, + Tolerations: []corev1.Toleration{ + { + Operator: corev1.TolerationOpExists, + }, + }, + }, + }, + }, + { + name: "netadmin profile", + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-XXX", + }, + }, + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileNetadmin, + CustomProfile: &corev1.Container{ + Env: []corev1.EnvVar{ + { + Name: "TEST_KEY", + Value: "TEST_VALUE", + }, + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Env: []corev1.EnvVar{ + { + Name: "TEST_KEY", + Value: "TEST_VALUE", + }, + }, + VolumeMounts: nil, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, + }, + }, + }, + }, + HostIPC: true, + HostNetwork: true, + HostPID: true, + NodeName: "node-XXX", + RestartPolicy: corev1.RestartPolicyNever, + Volumes: nil, + Tolerations: []corev1.Toleration{ + { + Operator: corev1.TolerationOpExists, + }, + }, + }, + }, + }, + { + name: "sysadmin profile", + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-XXX", + }, + }, + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileSysadmin, + CustomProfile: &corev1.Container{ + Env: []corev1.EnvVar{ + { + Name: "TEST_KEY", + Value: "TEST_VALUE", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "host-root", + ReadOnly: true, + MountPath: "/host", + }, + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Env: []corev1.EnvVar{ + { + Name: "TEST_KEY", + Value: "TEST_VALUE", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "host-root", + ReadOnly: true, + MountPath: "/host", + }, + }, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + HostIPC: true, + HostNetwork: true, + HostPID: true, + NodeName: "node-XXX", + RestartPolicy: corev1.RestartPolicyNever, + Volumes: []corev1.Volume{ + { + Name: "host-root", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/", + }, + }, + }, + }, + Tolerations: []corev1.Toleration{ + { + Operator: corev1.TolerationOpExists, + }, + }, + }, + }, + }, + } { + + t.Run(tc.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.DebugCustomProfile}, t, func(t *testing.T) { + var err error + tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile) + if err != nil { + t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) + } + tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() + + pod, err := tc.opts.generateNodeDebugPod(tc.node) + if err != nil { + t.Fatalf("Fail to generate node debug pod: %v", err) + } + tc.expected.Name = pod.Name + if diff := cmp.Diff(tc.expected, pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + }) + } +} + +func TestGenerateCopyDebugPodCustomProfile(t *testing.T) { + for _, tc := range []struct { + name string + copyPod *corev1.Pod + opts *DebugOptions + expected *corev1.Pod + }{ + { + name: "baseline profile", + copyPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + }, + }, + opts: &DebugOptions{ + SameNode: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileBaseline, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + Containers: []corev1.Container{ + { + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + Volumes: nil, + ShareProcessNamespace: pointer.Bool(true), + }, + }, + }, + { + name: "restricted profile", + copyPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + }, + }, + opts: &DebugOptions{ + SameNode: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileRestricted, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + Containers: []corev1.Container{ + { + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: pointer.Bool(false), + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + LocalhostProfile: nil, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + Volumes: nil, + ShareProcessNamespace: pointer.Bool(true), + }, + }, + }, + { + name: "sysadmin profile", + copyPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + }, + }, + opts: &DebugOptions{ + SameNode: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileRestricted, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + Containers: []corev1.Container{ + { + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: pointer.Bool(false), + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + LocalhostProfile: nil, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + Volumes: nil, + ShareProcessNamespace: pointer.Bool(true), + }, + }, + }, + } { + + t.Run(tc.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.DebugCustomProfile}, t, func(t *testing.T) { + var err error + tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile) + if err != nil { + t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) + } + tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() + + pod, dc, err := tc.opts.generatePodCopyWithDebugContainer(tc.copyPod) + if err != nil { + t.Fatalf("Fail to generate node debug pod: %v", err) + } + tc.expected.Spec.Containers[0].Name = dc + if diff := cmp.Diff(tc.expected, pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + }) + } +} + +func TestGenerateEphemeralDebugPodCustomProfile(t *testing.T) { + for _, tc := range []struct { + name string + copyPod *corev1.Pod + opts *DebugOptions + expected *corev1.Pod + }{ + { + name: "baseline profile", + copyPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + }, + }, + opts: &DebugOptions{ + SameNode: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileBaseline, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + Volumes: nil, + }, + }, + }, + { + name: "restricted profile", + copyPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + }, + }, + opts: &DebugOptions{ + SameNode: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileRestricted, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: pointer.Bool(false), + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + LocalhostProfile: nil, + }, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + Volumes: nil, + }, + }, + }, + { + name: "sysadmin profile", + copyPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + }, + }, + opts: &DebugOptions{ + SameNode: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileRestricted, + CustomProfile: &corev1.Container{ + ImagePullPolicy: corev1.PullNever, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + RunAsNonRoot: pointer.Bool(false), + }, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "test", + NodeName: "test-node", + EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + VolumeMounts: nil, + Stdin: true, + TTY: false, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: pointer.Bool(false), + RunAsNonRoot: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + LocalhostProfile: nil, + }, + }, + }, + }, + }, + HostIPC: false, + HostNetwork: false, + HostPID: false, + Volumes: nil, + }, + }, + }, + } { + + t.Run(tc.name, func(t *testing.T) { + cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.DebugCustomProfile}, t, func(t *testing.T) { + var err error + tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile) + if err != nil { + t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) + } + tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() + + pod, ec, err := tc.opts.generateDebugContainer(tc.copyPod) + if err != nil { + t.Fatalf("Fail to generate node debug pod: %v", err) + } + tc.expected.Spec.EphemeralContainers[0].Name = ec.Name + if diff := cmp.Diff(tc.expected, pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + }) + } +} + func TestCompleteAndValidate(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() diff --git a/pkg/cmd/util/helpers.go b/pkg/cmd/util/helpers.go index d9e401fa..e28263bb 100644 --- a/pkg/cmd/util/helpers.go +++ b/pkg/cmd/util/helpers.go @@ -431,6 +431,7 @@ const ( OpenAPIV3Patch FeatureGate = "KUBECTL_OPENAPIV3_PATCH" RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS" PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS" + DebugCustomProfile FeatureGate = "KUBECTL_DEBUG_CUSTOM_PROFILE" ) // IsEnabled returns true iff environment variable is set to true.