package k8score import ( "context" "fmt" "strings" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) // DefaultDebugImage is the default image for ephemeral debug containers. const DefaultDebugImage = "busybox:latest" const defaultDebugRunAsUser int64 = 65532 // EphemeralContainerOptions configures debug container creation. type EphemeralContainerOptions struct { Namespace string PodName string TargetContainer string // Container to share process namespace with Image string // Debug image (default: busybox:latest) ContainerName string // Name for ephemeral container (auto-generated if empty) } // CreateEphemeralContainer adds an ephemeral debug container to a pod. func CreateEphemeralContainer(ctx context.Context, client kubernetes.Interface, opts EphemeralContainerOptions) (*corev1.EphemeralContainer, error) { if client == nil { return nil, fmt.Errorf("kubernetes client not initialized") } if opts.Image == "" { opts.Image = DefaultDebugImage } if opts.ContainerName == "" { opts.ContainerName = fmt.Sprintf("debug-%d", time.Now().Unix()) } pod, err := client.CoreV1().Pods(opts.Namespace).Get(ctx, opts.PodName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("failed to get pod: %w", err) } ec := newEphemeralContainer(opts, nil) updateErr := updateEphemeralContainer(ctx, client, pod, ec) if updateErr != nil && isRestrictedPodSecurityError(updateErr) { pod, err = client.CoreV1().Pods(opts.Namespace).Get(ctx, opts.PodName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("failed to get pod: %w", err) } securityContext := debugContainerSecurityContext(pod, opts.TargetContainer) if securityContext != nil { ec = newEphemeralContainer(opts, securityContext) updateErr = updateEphemeralContainer(ctx, client, pod, ec) } } if updateErr != nil { return nil, fmt.Errorf("failed to create ephemeral container: %w", updateErr) } return &ec, nil } func newEphemeralContainer(opts EphemeralContainerOptions, securityContext *corev1.SecurityContext) corev1.EphemeralContainer { return corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: opts.ContainerName, Image: opts.Image, ImagePullPolicy: corev1.PullIfNotPresent, Stdin: true, TTY: true, TerminationMessagePolicy: corev1.TerminationMessageReadFile, SecurityContext: securityContext, }, TargetContainerName: opts.TargetContainer, } } func updateEphemeralContainer(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod, ec corev1.EphemeralContainer) error { updated := pod.DeepCopy() updated.Spec.EphemeralContainers = append(updated.Spec.EphemeralContainers, ec) _, err := client.CoreV1().Pods(updated.Namespace).UpdateEphemeralContainers( ctx, updated.Name, updated, metav1.UpdateOptions{}, ) return err } func isRestrictedPodSecurityError(err error) bool { if err == nil { return false } msg := err.Error() return strings.Contains(msg, "violates PodSecurity") && strings.Contains(msg, "restricted") } func debugContainerSecurityContext(pod *corev1.Pod, targetContainer string) *corev1.SecurityContext { if pod != nil && pod.Spec.OS != nil && pod.Spec.OS.Name == corev1.Windows { return nil } runAsUser := debugRunAsUser(pod, targetContainer) runAsGroup := debugRunAsGroup(pod, targetContainer) return &corev1.SecurityContext{ AllowPrivilegeEscalation: boolPtr(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: boolPtr(true), RunAsGroup: runAsGroup, RunAsUser: runAsUser, SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, } } // targetSecurityContext finds the target among regular containers and init // containers, since a native sidecar (an init container with restartPolicy // Always) can be a debug target too. func targetSecurityContext(pod *corev1.Pod, targetContainer string) *corev1.SecurityContext { for _, list := range [][]corev1.Container{pod.Spec.Containers, pod.Spec.InitContainers} { for i := range list { if list[i].Name == targetContainer { return list[i].SecurityContext } } } return nil } func debugRunAsUser(pod *corev1.Pod, targetContainer string) *int64 { if pod != nil { if sc := targetSecurityContext(pod, targetContainer); sc != nil && sc.RunAsUser != nil && *sc.RunAsUser != 0 { runAsUser := *sc.RunAsUser return &runAsUser } if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.RunAsUser != nil && *pod.Spec.SecurityContext.RunAsUser != 0 { runAsUser := *pod.Spec.SecurityContext.RunAsUser return &runAsUser } } runAsUser := defaultDebugRunAsUser return &runAsUser } func debugRunAsGroup(pod *corev1.Pod, targetContainer string) *int64 { if pod != nil { if sc := targetSecurityContext(pod, targetContainer); sc != nil && sc.RunAsGroup != nil && *sc.RunAsGroup != 0 { runAsGroup := *sc.RunAsGroup return &runAsGroup } if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.RunAsGroup != nil && *pod.Spec.SecurityContext.RunAsGroup != 0 { runAsGroup := *pod.Spec.SecurityContext.RunAsGroup return &runAsGroup } } return nil } // WaitForEphemeralContainer polls until an ephemeral container reaches Running state or timeout. func WaitForEphemeralContainer(ctx context.Context, client kubernetes.Interface, namespace, podName, containerName string, timeout time.Duration) error { if client == nil { return fmt.Errorf("kubernetes client not initialized") } deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("failed to get pod: %w", err) } for _, status := range pod.Status.EphemeralContainerStatuses { if status.Name == containerName { if status.State.Running != nil { return nil } if status.State.Terminated != nil { return fmt.Errorf("container terminated: %s", status.State.Terminated.Reason) } } } select { case <-ctx.Done(): return ctx.Err() case <-time.After(500 * time.Millisecond): continue } } return fmt.Errorf("timeout waiting for ephemeral container to start") }