github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/shell.go (about) 1 package cli 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "syscall" 11 12 "github.com/spf13/cobra" 13 apierrors "k8s.io/apimachinery/pkg/api/errors" 14 "k8s.io/apimachinery/pkg/types" 15 "k8s.io/cli-runtime/pkg/genericclioptions" 16 "sigs.k8s.io/controller-runtime/pkg/client" 17 18 "github.com/tilt-dev/tilt/internal/container" 19 "github.com/tilt-dev/tilt/internal/controllers/core/kubernetesdiscovery" 20 "github.com/tilt-dev/tilt/internal/k8s" 21 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 22 "github.com/tilt-dev/tilt/pkg/logger" 23 "github.com/tilt-dev/tilt/pkg/model" 24 ) 25 26 type shellCmd struct { 27 streams genericclioptions.IOStreams 28 container string 29 execer ShellExecer 30 } 31 32 func newShellCmd(streams genericclioptions.IOStreams) *shellCmd { 33 return &shellCmd{ 34 streams: streams, 35 execer: realShellExecer{}, 36 } 37 } 38 39 func (c *shellCmd) name() model.TiltSubcommand { return "shell" } 40 41 func (c *shellCmd) register() *cobra.Command { 42 cmd := &cobra.Command{ 43 Use: "shell [<resource-name>]", 44 DisableFlagsInUseLine: true, 45 Short: "Opens a shell into a container running in Tilt", 46 Long: `Opens a shell into a container running in Tilt. 47 48 Given a resource name, finds the Kubernetes Pod in that resource, 49 and opens an interactive shell. 50 51 By default, in order, we'll try: 52 - kubectl shell 53 - kubectl exec -it <pod> -- bash 54 - kubectl exec -it <pod> -- sh 55 56 Currently only works on MacOS and Linux.`, 57 Args: cobra.ExactArgs(1), 58 } 59 60 addConnectServerFlags(cmd) 61 cmd.Flags().StringVarP(&c.container, "container", "c", "", 62 "Name of the container within the pod. Only required if there is more than 1 container.") 63 64 return cmd 65 } 66 67 func (c *shellCmd) run(ctx context.Context, args []string) error { 68 ctx = logger.WithLogger(ctx, logger.NewLogger(logger.Get(ctx).Level(), c.streams.ErrOut)) 69 70 ctrlclient, err := newClient(ctx) 71 if err != nil { 72 return err 73 } 74 75 resourceName := args[0] 76 var uiResource v1alpha1.UIResource 77 err = ctrlclient.Get(ctx, types.NamespacedName{Name: resourceName}, &uiResource) 78 if err != nil { 79 if apierrors.IsNotFound(err) { 80 return fmt.Errorf("resource %s not found. To see available resources, run:\ntilt get uiresources", resourceName) 81 } 82 return fmt.Errorf("looking up resource %s: %v", resourceName, err) 83 } 84 85 // TODO(nicks): Add Docker Compose support. 86 if uiResource.Status.K8sResourceInfo == nil { 87 return fmt.Errorf("resource %s is not a Kubernetes resource. Only Kubernetes pods currently supported", 88 resourceName) 89 } 90 91 return c.runK8s(ctx, ctrlclient, resourceName) 92 } 93 94 func (c *shellCmd) runK8s(ctx context.Context, ctrlclient client.Client, resourceName string) error { 95 // TODO(nicks): Wait for the pod to become ready? 96 var k8sDisco v1alpha1.KubernetesDiscovery 97 err := ctrlclient.Get(ctx, types.NamespacedName{Name: resourceName}, &k8sDisco) 98 if err != nil { 99 return fmt.Errorf("looking up kubernetes status %s: %v", resourceName, err) 100 } 101 102 pod := kubernetesdiscovery.PickBestPortForwardPod(&k8sDisco) 103 if pod == nil { 104 return fmt.Errorf("no pod found for resource %s", resourceName) 105 } 106 107 co, err := c.selectContainer(pod) 108 if err != nil { 109 return err 110 } 111 112 kubectlBinary, err := c.execer.LookPath("kubectl") 113 if err != nil || kubectlBinary == "" { 114 return fmt.Errorf("could not find kubectl: %v", err) 115 } 116 117 // Fetch the kubeconfig that tilt manages, rather than using the kubeconfig of the current shell. 118 var cluster v1alpha1.Cluster 119 err = ctrlclient.Get(ctx, types.NamespacedName{Name: "default"}, &cluster) 120 if err != nil { 121 return fmt.Errorf("looking up cluster: %v", err) 122 } 123 if cluster.Status.Connection == nil || cluster.Status.Connection.Kubernetes == nil { 124 return fmt.Errorf("kubernetes cluster not connected: %v", cluster.Status.Error) 125 } 126 127 env := append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", cluster.Status.Connection.Kubernetes.ConfigPath)) 128 129 hasKubectlShell, err := c.execer.LookPath("kubectl-shell") 130 if err == nil && hasKubectlShell != "" { 131 cmd := model.Cmd{Argv: []string{"kubectl", "shell", pod.Name, "-n", pod.Namespace, "-c", co.Name}} 132 logger.Get(ctx).Infof("Running: %v", cmd) 133 return c.execer.Exec(kubectlBinary, cmd.Argv, env) 134 } 135 136 k8sClient, err := wireK8sClient(ctx) 137 if err != nil { 138 return fmt.Errorf("initializing k8s client: %v", err) 139 } 140 141 err = k8sClient.Exec(ctx, k8s.PodID(pod.Name), container.Name(co.Name), k8s.Namespace(pod.Namespace), 142 []string{"which", "bash"}, &bytes.Buffer{}, io.Discard, io.Discard) 143 if err == nil { 144 cmd := model.Cmd{Argv: []string{"kubectl", "exec", "-it", pod.Name, "-n", pod.Namespace, "-c", co.Name, "--", "bash"}} 145 logger.Get(ctx).Infof("Running: %v", cmd) 146 return c.execer.Exec(kubectlBinary, cmd.Argv, env) 147 } 148 149 err = k8sClient.Exec(ctx, k8s.PodID(pod.Name), container.Name(co.Name), k8s.Namespace(pod.Namespace), 150 []string{"which", "sh"}, &bytes.Buffer{}, io.Discard, io.Discard) 151 if err == nil { 152 cmd := model.Cmd{Argv: []string{"kubectl", "exec", "-it", pod.Name, "-n", pod.Namespace, "-c", co.Name, "--", "sh"}} 153 logger.Get(ctx).Infof("Running: %v", cmd) 154 return c.execer.Exec(kubectlBinary, cmd.Argv, env) 155 } 156 157 return fmt.Errorf(`could not find bash or sh in container image: %s 158 159 We're working on a new debugging tool for dynamically installing 160 a shell in a minimal container image. 161 162 https://hub.docker.com/extensions/docker/labs-k8s-toolkit-extension 163 164 To try it out, install the Docker Desktop extension, and run 'tilt shell' again.`, co.Image) 165 } 166 167 func (c *shellCmd) selectContainer(pod *v1alpha1.Pod) (v1alpha1.Container, error) { 168 if len(pod.InitContainers) == 0 && len(pod.Containers) == 1 { 169 if c.container != "" && pod.Containers[0].Name != c.container { 170 return v1alpha1.Container{}, fmt.Errorf("container %s not found in pod %s", c.container, pod.Name) 171 } 172 return pod.Containers[0], nil 173 } 174 175 for _, co := range pod.InitContainers { 176 if co.Name == c.container { 177 return co, nil 178 } 179 } 180 for _, co := range pod.Containers { 181 if co.Name == c.container { 182 return co, nil 183 } 184 } 185 return v1alpha1.Container{}, fmt.Errorf("container %s not found in pod %s", c.container, pod.Name) 186 } 187 188 type ShellExecer interface { 189 // Checks if the given binary exists in the PATH. 190 LookPath(file string) (string, error) 191 192 // Replaces the current process with the given binary. 193 Exec(binary string, argv []string, env []string) error 194 } 195 196 type realShellExecer struct{} 197 198 func (r realShellExecer) LookPath(file string) (string, error) { 199 return exec.LookPath(file) 200 } 201 func (r realShellExecer) Exec(binary string, argv []string, env []string) error { 202 return syscall.Exec(binary, argv, env) 203 }