Add custom debug profiles on top of static profiles
This PR adds `custom` flag to let user customizes debug resources. `custom` flag accepts partial container spec in json format.
This commit is contained in:
		| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Arda Güçlü
					Arda Güçlü