diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go index bd25c64ffad..4928d1c2324 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -121,6 +121,7 @@ type DebugOptions struct { TargetContainer string TTY bool Profile string + Applier ProfileApplier attachChanged bool shareProcessedChanged bool @@ -129,8 +130,6 @@ type DebugOptions struct { genericclioptions.IOStreams WarningPrinter *printers.WarningPrinter - - applier ProfileApplier } // NewDebugOptions returns a DebugOptions initialized with default values. @@ -180,7 +179,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.")) + cmd.Flags().StringVar(&opt.Profile, "profile", ProfileLegacy, i18n.T(`Debugging profile. Options are "legacy", "general", "baseline", or "restricted".`)) } // Complete finishes run-time initialization of debug.DebugOptions. @@ -226,9 +225,13 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st if o.WarningPrinter == nil { o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) } - o.applier, err = NewProfileApplier(o.Profile) - if err != nil { - return err + + if o.Applier == nil { + applier, err := NewProfileApplier(o.Profile) + if err != nil { + return err + } + o.Applier = applier } return nil @@ -536,10 +539,12 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *co copied := pod.DeepCopy() copied.Spec.EphemeralContainers = append(copied.Spec.EphemeralContainers, *ec) - if err := o.applier.Apply(copied, name, copied); err != nil { + if err := o.Applier.Apply(copied, name, copied); err != nil { return nil, nil, err } + ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1] + return copied, ec, nil } @@ -593,7 +598,7 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, err p.Spec.Containers[0].Command = o.Args } - if err := o.applier.Apply(p, cn, node); err != nil { + if err := o.Applier.Apply(p, cn, node); err != nil { return nil, err } @@ -614,7 +619,7 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core copied.Spec.EphemeralContainers = nil // change ShareProcessNamespace configuration only when commanded explicitly if o.shareProcessedChanged { - copied.Spec.ShareProcessNamespace = pointer.BoolPtr(o.ShareProcesses) + copied.Spec.ShareProcessNamespace = pointer.Bool(o.ShareProcesses) } if !o.SameNode { copied.Spec.NodeName = "" @@ -647,13 +652,11 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core if len(name) == 0 { name = o.computeDebugContainerName(copied) } - c = &corev1.Container{ + copied.Spec.Containers = append(copied.Spec.Containers, corev1.Container{ Name: name, TerminationMessagePolicy: corev1.TerminationMessageReadFile, - } - defer func() { - copied.Spec.Containers = append(copied.Spec.Containers, *c) - }() + }) + c = &copied.Spec.Containers[len(copied.Spec.Containers)-1] } if len(o.Args) > 0 { @@ -676,7 +679,7 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core c.Stdin = o.Interactive c.TTY = o.TTY - err := o.applier.Apply(copied, c.Name, pod) + err := o.Applier.Apply(copied, c.Name, pod) if err != nil { return nil, "", err } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go index 99e634e49d9..0bb42f28af0 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go @@ -55,6 +55,7 @@ func TestGenerateDebugContainer(t *testing.T) { Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -72,6 +73,7 @@ func TestGenerateDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, TargetContainer: "myapp", + Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -90,6 +92,7 @@ func TestGenerateDebugContainer(t *testing.T) { Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -109,6 +112,7 @@ func TestGenerateDebugContainer(t *testing.T) { Args: []string{"echo", "one", "two", "three"}, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -125,6 +129,7 @@ func TestGenerateDebugContainer(t *testing.T) { opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -140,6 +145,7 @@ func TestGenerateDebugContainer(t *testing.T) { opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ @@ -164,6 +170,7 @@ func TestGenerateDebugContainer(t *testing.T) { opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ @@ -196,6 +203,7 @@ func TestGenerateDebugContainer(t *testing.T) { opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileLegacy, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ @@ -227,6 +235,65 @@ func TestGenerateDebugContainer(t *testing.T) { }, }, }, + { + name: "general profile", + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileGeneral, + }, + expected: &corev1.EphemeralContainer{ + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + }, + { + name: "baseline profile", + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileBaseline, + }, + expected: &corev1.EphemeralContainer{ + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + { + name: "restricted profile", + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileRestricted, + }, + expected: &corev1.EphemeralContainer{ + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + }, } { t.Run(tc.name, func(t *testing.T) { tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() @@ -236,11 +303,11 @@ func TestGenerateDebugContainer(t *testing.T) { tc.pod = &corev1.Pod{} } - applier, err := NewProfileApplier(ProfileLegacy) + applier, err := NewProfileApplier(tc.opts.Profile) if err != nil { - t.Fatalf("fail to create %s profile", ProfileLegacy) + t.Fatalf("failed to create profile applier: %s: %v", tc.opts.Profile, err) } - tc.opts.applier = applier + tc.opts.Applier = applier _, debugContainer, err := tc.opts.generateDebugContainer(tc.pod) if err != nil { @@ -814,7 +881,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, - ShareProcessNamespace: pointer.BoolPtr(true), + ShareProcessNamespace: pointer.Bool(true), }, }, }, @@ -1017,7 +1084,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { var err error - tc.opts.applier, err = NewProfileApplier(ProfileLegacy) + tc.opts.Applier, err = NewProfileApplier(ProfileLegacy) if err != nil { t.Fatalf("Fail to create legacy profile: %v", err) } @@ -1212,7 +1279,7 @@ func TestGenerateNodeDebugPod(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { var err error - tc.opts.applier, err = NewProfileApplier(ProfileLegacy) + tc.opts.Applier, err = NewProfileApplier(ProfileLegacy) if err != nil { t.Fatalf("Fail to create legacy profile: %v", err) } @@ -1236,7 +1303,7 @@ func TestCompleteAndValidate(t *testing.T) { cmpFilter := cmp.FilterPath(func(p cmp.Path) bool { switch p.String() { // IOStreams contains unexported fields - case "IOStreams": + case "IOStreams", "Applier": return true } return false @@ -1572,7 +1639,6 @@ func TestCompleteAndValidate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { opts := NewDebugOptions(ioStreams) var gotError error - cmd := &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { gotError = opts.Complete(tf, cmd, args) @@ -1599,7 +1665,7 @@ func TestCompleteAndValidate(t *testing.T) { } if diff := cmp.Diff(tc.wantOpts, opts, cmpFilter, cmpopts.IgnoreFields(DebugOptions{}, - "attachChanged", "shareProcessedChanged", "podClient", "WarningPrinter", "applier")); diff != "" { + "attachChanged", "shareProcessedChanged", "podClient", "WarningPrinter", "Applier")); diff != "" { t.Error("CompleteAndValidate unexpected diff in generated object: (-want +got):\n", diff) } }) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go deleted file mode 100644 index d2a71a23aa4..00000000000 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profile_applier.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -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) -} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go index 07b14a6d6c1..4100e52dad1 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go @@ -21,39 +21,266 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubectl/pkg/util/podutils" + "k8s.io/utils/pointer" ) -// 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 { +type debugStyle int + +const ( + // debug by ephemeral container + ephemeral debugStyle = iota + // debug by pod copy + podCopy + // debug node + node + // unsupported debug methodology + unsupported +) + +const ( + // NOTE: when you add a new profile string, remember to add it to the + // --profile flag's help text + + // ProfileLegacy represents the legacy debugging profile which is backwards-compatible with 1.23 behavior. + ProfileLegacy = "legacy" + // ProfileGeneral contains a reasonable set of defaults tailored for each debugging journey. + ProfileGeneral = "general" + // ProfileBaseline is identical to "general" but eliminates privileges that are disallowed under + // the baseline security profile, such as host namespaces, host volume, mounts and SYS_PTRACE. + ProfileBaseline = "baseline" + // ProfileRestricted is identical to "baseline" but adds configuration that's required + // under the restricted security profile, such as requiring a non-root user and dropping all capabilities. + ProfileRestricted = "restricted" +) + +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 &legacyProfile{}, nil + case ProfileGeneral: + return &generalProfile{}, nil + case ProfileBaseline: + return &baselineProfile{}, nil + case ProfileRestricted: + return &restrictedProfile{}, nil + } + + return nil, fmt.Errorf("unknown profile: %s", profile) +} + +type legacyProfile struct { +} + +type generalProfile struct { +} + +type baselineProfile struct { +} + +type restrictedProfile struct { +} + +func (p *legacyProfile) Apply(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 + mountRootPartition(pod, containerName) + useHostNamespaces(pod) return nil default: return fmt.Errorf("the %s profile doesn't support objects of type %T", ProfileLegacy, target) } } + +func getDebugStyle(pod *corev1.Pod, target runtime.Object) (debugStyle, error) { + switch target.(type) { + case *corev1.Pod: + if asserted, ok := target.(*corev1.Pod); ok { + if pod != asserted { // comparing addresses + return podCopy, nil + } + } + return ephemeral, nil + case *corev1.Node: + return node, nil + } + return unsupported, fmt.Errorf("objects of type %T are not supported", target) +} + +func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { + style, err := getDebugStyle(pod, target) + if err != nil { + return fmt.Errorf("general profile: %s", err) + } + + switch style { + case node: + mountRootPartition(pod, containerName) + clearSecurityContext(pod, containerName) + useHostNamespaces(pod) + + case podCopy: + removeLabelsAndProbes(pod) + allowProcessTracing(pod, containerName) + shareProcessNamespace(pod) + + case ephemeral: + allowProcessTracing(pod, containerName) + } + + return nil +} + +func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { + style, err := getDebugStyle(pod, target) + if err != nil { + return fmt.Errorf("baseline profile: %s", err) + } + + clearSecurityContext(pod, containerName) + + switch style { + case podCopy: + removeLabelsAndProbes(pod) + shareProcessNamespace(pod) + + case ephemeral, node: + // no additional modifications needed + } + + return nil +} + +func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { + style, err := getDebugStyle(pod, target) + if err != nil { + return fmt.Errorf("restricted profile: %s", err) + } + + disallowRoot(pod, containerName) + dropCapabilities(pod, containerName) + + switch style { + case node: + clearSecurityContext(pod, containerName) + + case podCopy: + shareProcessNamespace(pod) + + case ephemeral: + // no additional modifications needed + } + + return nil +} + +// removeLabelsAndProbes removes labels from the pod and remove probes +// from all containers of the pod. +func removeLabelsAndProbes(p *corev1.Pod) { + p.Labels = nil + for i := range p.Spec.Containers { + p.Spec.Containers[i].LivenessProbe = nil + p.Spec.Containers[i].ReadinessProbe = nil + } +} + +// mountRootPartition mounts the host's root path at "/host" in the container. +func mountRootPartition(p *corev1.Pod, containerName string) { + const volumeName = "host-root" + p.Spec.Volumes = append(p.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: "/"}, + }, + }) + podutils.VisitContainers(&p.Spec, podutils.Containers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{ + MountPath: "/host", + Name: volumeName, + }) + return false + }) +} + +// useHostNamespaces configures the pod to use the host's network, PID, and IPC +// namespaces. +func useHostNamespaces(p *corev1.Pod) { + p.Spec.HostNetwork = true + p.Spec.HostPID = true + p.Spec.HostIPC = true +} + +// shareProcessNamespace configures all containers in the pod to share the +// process namespace. +func shareProcessNamespace(p *corev1.Pod) { + p.Spec.ShareProcessNamespace = pointer.BoolPtr(true) +} + +// clearSecurityContext clears the security context for the container. +func clearSecurityContext(p *corev1.Pod, containerName string) { + podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + c.SecurityContext = nil + return false + }) +} + +// disallowRoot configures the container to run as a non-root user. +func disallowRoot(p *corev1.Pod, containerName string) { + podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + c.SecurityContext = &corev1.SecurityContext{ + RunAsNonRoot: pointer.BoolPtr(true), + } + return false + }) +} + +// dropCapabilities drops all Capabilities for the container +func dropCapabilities(p *corev1.Pod, containerName string) { + podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + if c.SecurityContext == nil { + c.SecurityContext = &corev1.SecurityContext{} + } + c.SecurityContext.Capabilities = &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + } + return false + }) +} + +// allowProcessTracing grants the SYS_PTRACE capability to the container. +func allowProcessTracing(p *corev1.Pod, containerName string) { + podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + if c.SecurityContext == nil { + c.SecurityContext = &corev1.SecurityContext{} + } + if c.SecurityContext.Capabilities == nil { + c.SecurityContext.Capabilities = &corev1.Capabilities{} + } + c.SecurityContext.Capabilities.Add = append(c.SecurityContext.Capabilities.Add, "SYS_PTRACE") + return false + }) +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles_test.go new file mode 100644 index 00000000000..cdaaebbd897 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles_test.go @@ -0,0 +1,432 @@ +/* +Copyright 2020 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 ( + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" +) + +var testNode = &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-XXX", + }, +} + +func TestGeneralProfile(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + }, + }, + }}, + } + + tests := map[string]struct { + pod *corev1.Pod + containerName string + target runtime.Object + expectPod *corev1.Pod + expectErr bool + }{ + "bad inputs results in error": { + pod: nil, + containerName: "dbg", + target: runtime.Object(nil), + expectErr: true, + }, + "debug by ephemeral container": { + pod: pod, + containerName: "dbg", + target: pod, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + }}, + }, + }, + "debug by pod copy": { + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }, + }, + }, + }, + containerName: "dbg", + target: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + }, + }, + }, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN", "SYS_PTRACE"}, + }, + }, + }, + }, + ShareProcessNamespace: pointer.Bool(true), + }, + }, + }, + "debug by node": { + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: testNode, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + HostNetwork: true, + HostPID: true, + HostIPC: true, + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + VolumeMounts: []corev1.VolumeMount{ + { + MountPath: "/host", + Name: "host-root", + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "host-root", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: "/"}, + }, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := (&generalProfile{}).Apply(test.pod, test.containerName, test.target) + if (err != nil) != test.expectErr { + t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) + } + if err != nil { + return + } + if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + } +} + +func TestBaselineProfile(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + }}, + } + + tests := map[string]struct { + pod *corev1.Pod + containerName string + target runtime.Object + expectPod *corev1.Pod + expectErr bool + }{ + "bad inputs results in error": { + pod: nil, + containerName: "dbg", + target: runtime.Object(nil), + expectErr: true, + }, + "debug by ephemeral container": { + pod: pod, + containerName: "dbg", + target: pod, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + }, + }, + }}, + }, + }, + "debug by pod copy": { + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + }, + }, + }, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + ShareProcessNamespace: pointer.Bool(true), + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + }, + }, + }, + }, + }, + "debug by node": { + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: testNode, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := (&baselineProfile{}).Apply(test.pod, test.containerName, test.target) + if (err != nil) != test.expectErr { + t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) + } + if err != nil { + return + } + if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + } +} + +func TestRestrictedProfile(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + }}, + } + + tests := map[string]struct { + pod *corev1.Pod + containerName string + target runtime.Object + expectPod *corev1.Pod + expectErr bool + }{ + "bad inputs results in error": { + pod: nil, + containerName: "dbg", + target: runtime.Object(nil), + expectErr: true, + }, + "debug by ephemeral container": { + pod: pod, + containerName: "dbg", + target: pod, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + }}, + }, + }, + "debug by pod copy": { + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + }, + }, + }, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + ShareProcessNamespace: pointer.Bool(true), + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: pointer.Bool(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + }, + }, + }, + "debug by node": { + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: testNode, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := (&restrictedProfile{}).Apply(test.pod, test.containerName, test.target) + if (err != nil) != test.expectErr { + t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) + } + if err != nil { + return + } + if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/util/podutils/podutils.go b/staging/src/k8s.io/kubectl/pkg/util/podutils/podutils.go index 847eb7e88b4..ddd2b9dbe3d 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/podutils/podutils.go +++ b/staging/src/k8s.io/kubectl/pkg/util/podutils/podutils.go @@ -186,3 +186,55 @@ func maxContainerRestarts(pod *corev1.Pod) int { } return maxRestarts } + +// ContainerType and VisitContainers are taken from +// https://github.com/kubernetes/kubernetes/blob/master/pkg/api/v1/pod/util.go +// kubectl cannot directly import this due to project goals + +// ContainerType signifies container type +type ContainerType int + +const ( + // Containers is for normal containers + Containers ContainerType = 1 << iota + // InitContainers is for init containers + InitContainers + // EphemeralContainers is for ephemeral containers + EphemeralContainers +) + +// AllContainers specifies that all containers be visited. +const AllContainers ContainerType = (InitContainers | Containers | EphemeralContainers) + +// ContainerVisitor is called with each container spec, and returns true +// if visiting should continue. +type ContainerVisitor func(container *corev1.Container, containerType ContainerType) (shouldContinue bool) + +// VisitContainers invokes the visitor function with a pointer to every container +// spec in the given pod spec with type set in mask. If visitor returns false, +// visiting is short-circuited. VisitContainers returns true if visiting completes, +// false if visiting was short-circuited. +func VisitContainers(podSpec *corev1.PodSpec, mask ContainerType, visitor ContainerVisitor) bool { + if mask&InitContainers != 0 { + for i := range podSpec.InitContainers { + if !visitor(&podSpec.InitContainers[i], InitContainers) { + return false + } + } + } + if mask&Containers != 0 { + for i := range podSpec.Containers { + if !visitor(&podSpec.Containers[i], Containers) { + return false + } + } + } + if mask&EphemeralContainers != 0 { + for i := range podSpec.EphemeralContainers { + if !visitor((*corev1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) { + return false + } + } + } + return true +}