Support `extra_pod_selectors` in `port_forward()` for fine-grained pod targeting
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.
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()?
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
)