tilt icon indicating copy to clipboard operation
tilt copied to clipboard

Support `extra_pod_selectors` in `port_forward()` for fine-grained pod targeting

Open beatak opened this issue 6 months ago • 2 comments

Describe the Feature You Want

I’d like port_forward() to support an extra_pod_selectors argument (just like k8s_resource() does), so I can specify pod selection logic per port forward, instead of only at the resource level.

This would allow helm_resource() or k8s_resource() to define multiple port forwards targeting different Pods under the same resource, each with its own selector.

Example:

helm_resource(
    'local-deps',
    '../environments/local',
    namespace=ns_local,
    port_forwards=[
        port_forward(
            19000,
            container_port=9000,
            extra_pod_selectors={'app': 'minio'},
            name='Minio'
        )
    ]
)

Current Behavior

Currently, if I want to control which pod a port forward attaches to, I must set extra_pod_selectors at the k8s_resource() level, like this:

k8s_resource(
    workload='local-deps',
    extra_pod_selectors={'app': 'minio'},
    discovery_strategy='selectors-only',
    port_forwards=[port_forward(19000, container_port=9000, name='Minio')]
)

This works but it forces me to break out what logically belongs to the same helm_resource() just to handle one port forward.

Why Do You Want This?

My Tiltfile installs a monolithic Helm chart via helm_resource() that sets up many dependent services (e.g. MinIO, Postgres, Redis, etc.) in one go:

helm_resource(
    'local-deps',
    '../environments/local',
    namespace=ns_local,
    resource_deps=[...],
    update_dependencies=True,
    flags=[...],
    labels=['dependencies']
)

I’d like to port-forward into one or more of those components without splitting them into separate k8s_resource() declarations or overriding discovery for the entire resource.

Having extra_pod_selectors in port_forward() would simplify the Tiltfile. It feels like a natural extension, especially for complex Helm setups with multiple components running under a single release.

Additional context

This would be helpful in Helm-based environments where it’s common to have multiple services managed under one umbrella, I think.

I haven’t worked with the Tilt codebase before, but I’d be happy to dig into the implementation and contribute if there’s interest and some guidance from the maintainers.

beatak avatar Jun 27 '25 17:06 beatak

Actually, I tried the current workaround of defining multiple k8s_resource() calls with the same workload, each using a different extra_pod_selectors and port_forward() like this:

k8s_resource(
    workload='local-deps',
    extra_pod_selectors={'app': 'postgresql'},
    discovery_strategy='selectors-only',
    port_forwards=[
        port_forward(15432, container_port=5432, name="Postgres")
    ]
)

k8s_resource(
    workload='local-deps',
    extra_pod_selectors={'app': 'minio'},
    discovery_strategy='selectors-only', 
    port_forwards=[
        port_forward(9000, container_port=9000, name="MinIO API"),
        port_forward(9001, container_port=9001, name="MinIO Console")
    ]
)

It turns out this only works if there’s a single k8s_resource() declaration for the given workload. If I have more than one targeting the same workload (even with different selectors), only one of them actually works. "Links" shows up, but if I click it, I get "Reconnecting... Error port-forwarding local-deps (15432 -> 5432): lost connection to pod"

I’m not sure if this is a known limitation, a bug, or something I should file separately. Would love to hear if this behavior is expected. And maybe, this strengthens the case for supporting extra_pod_selectors directly inside port_forward()?

beatak avatar Jun 27 '25 21:06 beatak

I handled this in a similar way. I ended up making a helm_attach extension which works basically the same as k8s_attach except that it allows for a dependency on a helm_resource. When something like an image update causes the helm_resource to update, the helm_attach resource will restart after it is finished.

def helm_attach(name, chart_resource, kind="deployment", resource_name=None, namespace=None, **kwargs):
  """Creates a resource by attaching to a k8s object created from existing helm_resource. Reloads when the helm chart is
  updated/re-applied/etc.

  Args:
    name: Name of this resource
    chart_resource: Name of the Helm chart resource that manages this deployment.
    obj_kind: The kind of object to attach to, as you would specify to kubectl. Defaults to 'deployment'
    obj_name: Optional name of the specific k8s object to attach to. Defaults to value from `name`.
    namespace: Optional namespace where the deployment resides.
    **kwargs: Additional keyword arguments to pass to k8s_resource.
  """
  resource_name = resource_name if resource_name != None else name

  k8s_custom_deploy(
    name,
    apply_cmd="kubectl get -o=yaml %s/%s %s" % (obj_kind, obj_name, "--namespace=%s" % namespace if namespace else ""),
    deps=[],
    delete_cmd=["echo", "Skipping delete. Deployment %s managed by helm_resource: %s" % (name, chart_resource)],
  )

  k8s_resource(
    name,
    resource_deps=[chart_resource],
    **kwargs
  )

Example usage:

load('ext://helm_resource', 'helm_resource')
load('./helm_attach.Tiltfile', 'helm_attach')

helm_resource(
  'app-chart',
  './charts/release'
  # image_deps, etc.
)

helm_attach(
  'ui',
  obj_kind='deployment'
  obj_name='app-ui'
  chart_resource='app-chart',
  port_forwards=[port_forward(8080, 9091, name="UI")],
  links=[
    link('localhost:8080/admin', 'Admin UI'),
  ],
  # anything else will get passed to k8s_resource, including extra_pod_selectors
)

lachieh avatar Nov 06 '25 17:11 lachieh