pkg/k8score/exec.go
Rama main · réplica de lectura del repositorio original.
package k8score
import (
"fmt"
"slices"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
rcpkg "github.com/skyhook-io/radar/pkg/remotecommand"
)
// NewPodExecExecutor creates an executor for running commands in a pod container.
// Tries WebSocket first (k8s ≥1.29); falls back to SPDY on upgrade failure for older clusters.
// The caller uses the returned Executor to call StreamWithContext.
func NewPodExecExecutor(client kubernetes.Interface, config *rest.Config, namespace, podName, containerName string, command []string, tty bool) (remotecommand.Executor, error) {
return newPodExecutor(client, config, namespace, podName, &corev1.PodExecOptions{
Container: containerName,
Command: command,
Stdin: true,
Stdout: true,
// When TTY is true, the terminal muxes stderr into stdout, so Stderr must be false.
// Setting both TTY and Stderr causes stream errors on some API servers; matches kubectl exec -it.
Stderr: !tty,
TTY: tty,
})
}
// NewPodCommandExecutor creates an executor for a non-interactive command:
// no stdin and no TTY, with stdout and stderr kept apart, like
// `kubectl exec pod -- cmd`. Without stdin the command sees EOF at once
// instead of waiting on a stream nobody writes to.
func NewPodCommandExecutor(client kubernetes.Interface, config *rest.Config, namespace, podName, containerName string, command []string) (remotecommand.Executor, error) {
return newPodExecutor(client, config, namespace, podName, &corev1.PodExecOptions{
Container: containerName,
Command: command,
Stdout: true,
Stderr: true,
})
}
func newPodExecutor(client kubernetes.Interface, config *rest.Config, namespace, podName string, opts *corev1.PodExecOptions) (remotecommand.Executor, error) {
if client == nil {
return nil, fmt.Errorf("kubernetes client not initialized")
}
req := client.CoreV1().RESTClient().Post().
Resource("pods").
Name(podName).
Namespace(namespace).
SubResource("exec").
VersionedParams(opts, scheme.ParameterCodec)
executor, err := rcpkg.NewExecutor(config, req.URL())
if err != nil {
return nil, fmt.Errorf("failed to create exec executor for %s/%s/%s: %w", namespace, podName, opts.Container, err)
}
return executor, nil
}
// DefaultContainerAnnotation is the client-side convention kubectl, k9s, and
// Lens use to mark the "main" container of a multi-container pod. Service
// meshes (Istio, Linkerd, ASM) set it during injection to point past their
// sidecar. Clients that simply default to containers[0] can land in a
// distroless mesh proxy with no shell.
const DefaultContainerAnnotation = "kubectl.kubernetes.io/default-container"
// DefaultExecContainer picks the container to target when the request doesn't
// name one. It honors DefaultContainerAnnotation (matching kubectl exec
// behavior). Falls back to the first non-mesh app container; returns "" when none is available.
func DefaultExecContainer(pod *corev1.Pod) string {
if name := pod.Annotations[DefaultContainerAnnotation]; name != "" && slices.Contains(ExecContainerNames(pod), name) {
return name
}
for _, c := range pod.Spec.Containers {
if MeshProxyContainers[c.Name] == "" {
return c.Name
}
}
return ""
}
// MeshProxyContainers identifies injected infrastructure proxies.
var MeshProxyContainers = map[string]string{
"istio-proxy": "Istio",
"linkerd-proxy": "Linkerd",
"consul-dataplane": "Consul",
}
// ExecContainerNames includes regular, init and ephemeral containers.
// The API server enforces whether a selected container is running.
func ExecContainerNames(pod *corev1.Pod) []string {
names := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)+len(pod.Spec.EphemeralContainers))
for _, c := range pod.Spec.Containers {
names = append(names, c.Name)
}
for _, c := range pod.Spec.InitContainers {
names = append(names, c.Name)
}
for _, c := range pod.Spec.EphemeralContainers {
names = append(names, c.Name)
}
return names
}