flyteplugins-union 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {flyteplugins_union-0.4.2/src/flyteplugins_union.egg-info → flyteplugins_union-0.5.0}/PKG-INFO +21 -3
  2. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/README.md +17 -1
  3. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/licenses/BSL.txt +1 -1
  4. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/pyproject.toml +18 -1
  5. flyteplugins_union-0.5.0/src/flyteplugins/union/__init__.py +10 -0
  6. flyteplugins_union-0.5.0/src/flyteplugins/union/_debug.py +110 -0
  7. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/assignment.py +15 -9
  8. flyteplugins_union-0.5.0/src/flyteplugins/union/cli/debug.py +260 -0
  9. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/errors.py +13 -0
  10. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_base_volume.py +333 -61
  11. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/_backend.py +33 -3
  12. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/backend.py +24 -0
  13. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_ro_volume.py +13 -14
  14. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_rw_volume.py +17 -10
  15. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_volume_transformer.py +3 -4
  16. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/__init__.py +2 -0
  17. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_assignment.py +51 -16
  18. flyteplugins_union-0.5.0/src/flyteplugins/union/remote/_ssh_debug.py +542 -0
  19. flyteplugins_union-0.5.0/src/flyteplugins/union/ws_proxy.py +86 -0
  20. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0/src/flyteplugins_union.egg-info}/PKG-INFO +21 -3
  21. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins_union.egg-info/SOURCES.txt +6 -0
  22. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins_union.egg-info/entry_points.txt +4 -0
  23. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins_union.egg-info/requires.txt +3 -1
  24. flyteplugins_union-0.5.0/src/flyteplugins_union.egg-info/scm_file_list.json +173 -0
  25. flyteplugins_union-0.5.0/src/flyteplugins_union.egg-info/scm_version.json +8 -0
  26. flyteplugins_union-0.4.2/src/flyteplugins/union/__init__.py +0 -4
  27. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/LICENSE +0 -0
  28. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/README_PYPI.md +0 -0
  29. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/licenses/APL.txt +0 -0
  30. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/setup.cfg +0 -0
  31. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/__init__.py +0 -0
  32. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/_tui/__init__.py +0 -0
  33. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/_tui/_volume_explore.py +0 -0
  34. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/_volume_index.py +0 -0
  35. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/_volume_session.py +0 -0
  36. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/api_key.py +0 -0
  37. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/cluster.py +0 -0
  38. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/cluster_pool.py +0 -0
  39. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/member.py +0 -0
  40. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/policy.py +0 -0
  41. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/queue.py +0 -0
  42. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/role.py +0 -0
  43. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/user.py +0 -0
  44. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/cli/volume.py +0 -0
  45. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/__init__.py +0 -0
  46. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/authorizer_connect.py +0 -0
  47. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/authorizer_pb2.py +0 -0
  48. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/authorizer_pb2.pyi +0 -0
  49. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/definition_pb2.py +0 -0
  50. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/definition_pb2.pyi +0 -0
  51. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/payload_pb2.py +0 -0
  52. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/authorizer/payload_pb2.pyi +0 -0
  53. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/cluster_connect.py +0 -0
  54. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/cluster_pb2.py +0 -0
  55. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/cluster_pb2.pyi +0 -0
  56. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/definition_pb2.py +0 -0
  57. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/definition_pb2.pyi +0 -0
  58. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/payload_pb2.py +0 -0
  59. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/cluster/payload_pb2.pyi +0 -0
  60. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/clusterpool/clusterpool_connect.py +0 -0
  61. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/clusterpool/clusterpool_pb2.py +0 -0
  62. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/clusterpool/clusterpool_pb2.pyi +0 -0
  63. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/clusterpool/payload_pb2.py +0 -0
  64. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/clusterpool/payload_pb2.pyi +0 -0
  65. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/authorization_pb2.py +0 -0
  66. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/authorization_pb2.pyi +0 -0
  67. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/cluster_pb2.py +0 -0
  68. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/cluster_pb2.pyi +0 -0
  69. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/deployment_pb2.py +0 -0
  70. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/deployment_pb2.pyi +0 -0
  71. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/identifier_pb2.py +0 -0
  72. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/identifier_pb2.pyi +0 -0
  73. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/identity_pb2.py +0 -0
  74. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/identity_pb2.pyi +0 -0
  75. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/list_pb2.py +0 -0
  76. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/list_pb2.pyi +0 -0
  77. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/policy_pb2.py +0 -0
  78. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/policy_pb2.pyi +0 -0
  79. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/role_pb2.py +0 -0
  80. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/common/role_pb2.pyi +0 -0
  81. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_definition_pb2.py +0 -0
  82. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_definition_pb2.pyi +0 -0
  83. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_payload_pb2.py +0 -0
  84. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_payload_pb2.pyi +0 -0
  85. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_service_connect.py +0 -0
  86. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_service_pb2.py +0 -0
  87. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/app_service_pb2.pyi +0 -0
  88. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/enums_pb2.py +0 -0
  89. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/enums_pb2.pyi +0 -0
  90. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/member_payload_pb2.py +0 -0
  91. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/member_payload_pb2.pyi +0 -0
  92. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/member_service_connect.py +0 -0
  93. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/member_service_pb2.py +0 -0
  94. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/member_service_pb2.pyi +0 -0
  95. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/policy_payload_pb2.py +0 -0
  96. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/policy_payload_pb2.pyi +0 -0
  97. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/policy_service_connect.py +0 -0
  98. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/policy_service_pb2.py +0 -0
  99. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/policy_service_pb2.pyi +0 -0
  100. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/role_payload_pb2.py +0 -0
  101. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/role_payload_pb2.pyi +0 -0
  102. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/role_service_connect.py +0 -0
  103. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/role_service_pb2.py +0 -0
  104. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/role_service_pb2.pyi +0 -0
  105. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/user_payload_pb2.py +0 -0
  106. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/user_payload_pb2.pyi +0 -0
  107. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/user_service_connect.py +0 -0
  108. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/user_service_pb2.py +0 -0
  109. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/identity/user_service_pb2.pyi +0 -0
  110. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/queue/queue_connect.py +0 -0
  111. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/queue/queue_pb2.py +0 -0
  112. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/queue/queue_pb2.pyi +0 -0
  113. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/validate/__init__.py +0 -0
  114. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/validate/validate/__init__.py +0 -0
  115. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/internal/validate/validate/validate_pb2.py +0 -0
  116. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/__init__.py +0 -0
  117. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/__init__.py +0 -0
  118. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/__init__.py +0 -0
  119. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/_common.py +0 -0
  120. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/_metadata.py +0 -0
  121. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/_redis.py +0 -0
  122. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/_sqlite.py +0 -0
  123. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/io/_internal/_juicefs/bin/.gitkeep +0 -0
  124. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_api_key.py +0 -0
  125. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_cluster.py +0 -0
  126. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_cluster_pool.py +0 -0
  127. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_member.py +0 -0
  128. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_policy.py +0 -0
  129. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_queue.py +0 -0
  130. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_role.py +0 -0
  131. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_user.py +0 -0
  132. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/remote/_volume_explore.py +0 -0
  133. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/utils/__init__.py +0 -0
  134. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/utils/auth.py +0 -0
  135. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins/union/utils/image.py +0 -0
  136. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins_union.egg-info/dependency_links.txt +0 -0
  137. {flyteplugins_union-0.4.2 → flyteplugins_union-0.5.0}/src/flyteplugins_union.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flyteplugins-union
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Union SDK - Proprietary extensions for Flyte
5
5
  Author-email: unionai <info@union.ai>
6
6
  Project-URL: Homepage, https://www.union.ai/
@@ -19,8 +19,10 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: aiosqlite>=0.20.0
21
21
  Requires-Dist: connectrpc>=0.9.0
22
- Requires-Dist: flyte<2.5.0,>=2.3.0
22
+ Requires-Dist: flyte<2.6.0,>=2.5.7
23
23
  Requires-Dist: flyteidl2>=2.0.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: httpx-ws>=0.6.0
24
26
  Requires-Dist: kubernetes>=27.2.0
25
27
  Requires-Dist: pyyaml>=6.0
26
28
  Requires-Dist: rich-click>=1.8.0
@@ -43,6 +45,22 @@ For development:
43
45
  uv pip install -e .
44
46
  ```
45
47
 
48
+ ## Flyte compatibility
49
+
50
+ `flyteplugins-union` builds on `flyte` SDK internals that can change between `flyte`
51
+ **minor** releases, so each release pins `flyte` to a single minor line. **Install a
52
+ `flyteplugins-union` release built for your `flyte` minor:**
53
+
54
+ | flyteplugins-union | Requires flyte |
55
+ | ------------------ | ---------------- |
56
+ | 0.5.0+ | `>=2.5.7,<2.6.0` |
57
+ | 0.4.3 | `>=2.5.0,<2.6.0` |
58
+ | 0.4.0 – 0.4.2 | `>=2.3.0,<2.5.0` |
59
+ | 0.3.0 | `>=2.1.0,<2.5.0` |
60
+
61
+ See [COMPATIBILITY.md](https://github.com/unionai/flyteplugins-union/blob/main/COMPATIBILITY.md)
62
+ for the rationale and how the pin tracks new `flyte` releases.
63
+
46
64
  ## Features
47
65
 
48
66
  ### API Key Management
@@ -100,7 +118,7 @@ This makes Union commands available seamlessly through the Flyte CLI.
100
118
 
101
119
  ## Dependencies
102
120
 
103
- - flyte>=2.0.0b26 - The Flyte SDK v2
121
+ - flyte - The Flyte SDK v2 (pinned to a single minor; see [Flyte compatibility](#flyte-compatibility))
104
122
  - rich-click>=1.8.0 - For CLI formatting
105
123
 
106
124
  ## Development
@@ -13,6 +13,22 @@ For development:
13
13
  uv pip install -e .
14
14
  ```
15
15
 
16
+ ## Flyte compatibility
17
+
18
+ `flyteplugins-union` builds on `flyte` SDK internals that can change between `flyte`
19
+ **minor** releases, so each release pins `flyte` to a single minor line. **Install a
20
+ `flyteplugins-union` release built for your `flyte` minor:**
21
+
22
+ | flyteplugins-union | Requires flyte |
23
+ | ------------------ | ---------------- |
24
+ | 0.5.0+ | `>=2.5.7,<2.6.0` |
25
+ | 0.4.3 | `>=2.5.0,<2.6.0` |
26
+ | 0.4.0 – 0.4.2 | `>=2.3.0,<2.5.0` |
27
+ | 0.3.0 | `>=2.1.0,<2.5.0` |
28
+
29
+ See [COMPATIBILITY.md](https://github.com/unionai/flyteplugins-union/blob/main/COMPATIBILITY.md)
30
+ for the rationale and how the pin tracks new `flyte` releases.
31
+
16
32
  ## Features
17
33
 
18
34
  ### API Key Management
@@ -70,7 +86,7 @@ This makes Union commands available seamlessly through the Flyte CLI.
70
86
 
71
87
  ## Dependencies
72
88
 
73
- - flyte>=2.0.0b26 - The Flyte SDK v2
89
+ - flyte - The Flyte SDK v2 (pinned to a single minor; see [Flyte compatibility](#flyte-compatibility))
74
90
  - rich-click>=1.8.0 - For CLI formatting
75
91
 
76
92
  ## Development
@@ -17,7 +17,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
17
17
  computer programs whose source code are controlled by
18
18
  such third parties.
19
19
 
20
- Change Date: 2030-06-16
20
+ Change Date: 2030-06-29
21
21
 
22
22
  Change License: Apache License, Version 2.0
23
23
 
@@ -12,8 +12,11 @@ requires-python = ">=3.10"
12
12
  dependencies = [
13
13
  "aiosqlite>=0.20.0",
14
14
  "connectrpc>=0.9.0",
15
- "flyte>=2.3.0,<2.5.0",
15
+ "flyte>=2.5.7,<2.6.0",
16
16
  "flyteidl2>=2.0.0",
17
+ # ws_proxy (flyte-ws-proxy ssh ProxyCommand) speaks WebSocket over httpx.
18
+ "httpx>=0.27.0",
19
+ "httpx-ws>=0.6.0",
17
20
  "kubernetes>=27.2.0",
18
21
  "pyyaml>=6.0",
19
22
  "rich-click>=1.8.0",
@@ -74,6 +77,10 @@ exclude = ["src/flyteplugins/union/internal"]
74
77
  Homepage = "https://www.union.ai/"
75
78
  Documentation = "https://docs.union.ai/"
76
79
 
80
+ [project.scripts]
81
+ # Stdio<->WebSocket proxy used as the ssh ProxyCommand for SSH-into-task debug.
82
+ flyte-ws-proxy = "flyteplugins.union.ws_proxy:main"
83
+
77
84
  [project.entry-points."flyte.plugins.cli.commands"]
78
85
  # CLI plugins for Union commands
79
86
  "create.api-key" = "flyteplugins.union.cli.api_key:create_api_key"
@@ -109,6 +116,10 @@ Documentation = "https://docs.union.ai/"
109
116
  # than an `explore.volume` entry because the plugin loader won't auto-create a
110
117
  # missing `explore` verb group (it warns + skips).
111
118
  "explore" = "flyteplugins.union.cli.volume:explore"
119
+ # Top-level `flyte debug <run> [action]` command: relaunch an existing run with
120
+ # ssh-debug (via flyte.rerun) and connect — the single ssh-into-task entry point.
121
+ # A single command (no group needed).
122
+ "debug" = "flyteplugins.union.cli.debug:debug"
112
123
 
113
124
  [project.entry-points."flyte.plugins.types"]
114
125
  # Custom TypeTransformer that auto-commits a still-mounted Volume at
@@ -177,3 +188,9 @@ select = [
177
188
  # VolumeExplore lazy-imports flyte.io / msgpack / boto3 / the Volume model so
178
189
  # `import flyteplugins.union.remote` stays light for non-volume callers.
179
190
  "src/flyteplugins/union/remote/_volume_explore.py" = ["PLC0415"]
191
+ # ssh-debug client lazy-imports the flyte auth machinery / remote objects so
192
+ # `flyte debug --help` and `import flyteplugins.union.remote` stay light.
193
+ "src/flyteplugins/union/cli/debug.py" = ["PLC0415"]
194
+ # _debug defers the ssh-debug import (launch_env) so `import flyteplugins.union` stays light.
195
+ "src/flyteplugins/union/_debug.py" = ["PLC0415"]
196
+ "src/flyteplugins/union/remote/_ssh_debug.py" = ["PLC0415"]
@@ -0,0 +1,10 @@
1
+ """Union SDK - Proprietary extensions for Flyte.
2
+
3
+ This package provides Union-specific functionality on top of the open-source Flyte SDK.
4
+ """
5
+
6
+ from flyteplugins.union._debug import debug, with_debugcontext
7
+
8
+ # `SSHDebug` lives in `flyteplugins.union.remote` — import it from there
9
+ # (`from flyteplugins.union.remote import SSHDebug`).
10
+ __all__ = ["debug", "with_debugcontext"]
@@ -0,0 +1,110 @@
1
+ """SSH-into-task debug helpers.
2
+
3
+ Public surface:
4
+
5
+ - ``debug(...)`` / ``with_debugcontext(...)`` — re-exported from ``flyteplugins.union``.
6
+ - ``SSHDebug`` — resolve the connection; import it from ``flyteplugins.union.remote``.
7
+
8
+ `debug(...)` is the convenience for the two common cases (it just wraps `with_debugcontext`):
9
+
10
+ ```python
11
+ import flyte
12
+ from flyteplugins.union import debug
13
+ from flyteplugins.union.remote import SSHDebug
14
+
15
+ flyte.init_from_config()
16
+
17
+ run = debug(my_task, x=1) # launch a NEW task in debug mode
18
+ run = debug("some-prior-run") # relaunch an EXISTING run in debug mode (fetches its code+inputs)
19
+
20
+ print(SSHDebug.connect(run.name).ssh_config) # paste into ~/.ssh/config, then `ssh flyte-debug`
21
+ ```
22
+
23
+ The `flyte debug <run>` CLI relaunches **and** connects in one shot.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import TYPE_CHECKING, Any, Dict, Optional
29
+
30
+ import flyte
31
+
32
+ if TYPE_CHECKING:
33
+ from flyte import TaskTemplate
34
+ from flyte.remote import Run
35
+
36
+ __all__ = ["debug", "with_debugcontext"]
37
+
38
+
39
+ def with_debugcontext(
40
+ mode: Any = None,
41
+ *,
42
+ env_vars: Optional[Dict[str, str]] = None,
43
+ ssh_host_name: Optional[str] = None,
44
+ custom_context: Optional[Dict[str, str]] = None,
45
+ **kwargs,
46
+ ):
47
+ """Like `flyte.with_runcontext`, but preconfigured for ssh-into-task debug.
48
+
49
+ Ensures the auto-managed debug keypair exists and merges the ssh-debug env
50
+ (``_F_E_SSH`` / ``_F_SSH_PK`` / ``_F_E_VS``) into *env_vars*; all other
51
+ arguments are forwarded unchanged. Returns the same runner as
52
+ `with_runcontext`, so call ``.run(task, ...)`` / ``.rerun(run, ...)`` on it.
53
+
54
+ *ssh_host_name* records the intended ssh Host alias on the run's *custom_context*
55
+ (under ``ssh-host-name``); it is propagation/record only — the alias used to build
56
+ the local ssh-config is the one threaded directly to `SSHDebug.connect`.
57
+ """
58
+ from flyteplugins.union.remote._ssh_debug import SSH_HOST_NAME_CONTEXT_KEY, launch_env
59
+
60
+ cc = dict(custom_context or {})
61
+ if ssh_host_name:
62
+ cc[SSH_HOST_NAME_CONTEXT_KEY] = ssh_host_name
63
+ return flyte.with_runcontext(mode, env_vars=launch_env(env_vars), custom_context=cc or None, **kwargs)
64
+
65
+
66
+ def debug(
67
+ target: "str | TaskTemplate",
68
+ *args: Any,
69
+ action_name: str = "a0",
70
+ name: Optional[str] = None,
71
+ ssh_host_name: Optional[str] = None,
72
+ custom_context: Optional[Dict[str, str]] = None,
73
+ task_template: "Optional[TaskTemplate]" = None,
74
+ inputs: Optional[Dict[str, Any]] = None,
75
+ env_vars: Optional[Dict[str, str]] = None,
76
+ **kwargs: Any,
77
+ ) -> "Run":
78
+ """Launch a task, or relaunch an existing run, with ssh-into-task debug enabled. Returns the `Run`.
79
+
80
+ Two forms (mirroring `flyte.run` / `flyte.rerun`), both with the ssh-debug env injected so the new
81
+ run comes up with sshd:
82
+
83
+ - **`debug(task, x=1)`** — run ``task`` in debug mode (= `with_debugcontext().run(task, x=1)`).
84
+ - **`debug("run-name")`** — relaunch an existing run in debug mode, fetching its code + inputs
85
+ (= `with_debugcontext().rerun("run-name")`). Pass ``inputs={...}`` to change parameters or
86
+ ``task_template=`` to substitute code.
87
+
88
+ Then connect with `SSHDebug.connect(run.name)` (or use the `flyte debug <run>` CLI, which relaunches
89
+ **and** connects in one shot).
90
+
91
+ :param target: a `TaskTemplate` to launch, or a prior run name (str) to relaunch.
92
+ :param action_name: (run-name form) action to source the task + inputs from (default ``a0``).
93
+ :param name: explicit name for the new run; omit to let the platform assign one. A fixed name
94
+ makes the launch idempotent — relaunching with the same name re-uses the existing run.
95
+ :param ssh_host_name: record the intended ssh Host alias on the run's custom_context
96
+ (``ssh-host-name``); record/propagation only (see `with_debugcontext`).
97
+ :param custom_context: extra custom-context key/values to attach to the run (merged with
98
+ ``ssh-host-name`` when *ssh_host_name* is given).
99
+ :param task_template: (run-name form) substitute task to run instead of the prior run's code.
100
+ :param inputs: (run-name form) native input overrides; omit to reuse the prior run's inputs.
101
+ :param env_vars: extra env vars to set on the run (merged with the ssh-debug env).
102
+ :param args, kwargs: (task form) inputs to pass to ``target`` (like `flyte.run(task, *args, **kwargs)`).
103
+ :return: the new ssh-debug Run.
104
+ """
105
+ # TODO: add a `recover=` param (recover from the run being relaunched) once the flyteidl2
106
+ # RunSpec.recover field + backend support ship.
107
+ ctx = with_debugcontext(name=name, env_vars=env_vars, ssh_host_name=ssh_host_name, custom_context=custom_context)
108
+ if isinstance(target, str):
109
+ return ctx.rerun(target, action_name, task_template=task_template, inputs=inputs)
110
+ return ctx.run(target, *args, **kwargs)
@@ -51,24 +51,26 @@ def create_assignment(
51
51
  @click.command("assignment")
52
52
  @click.option("--user-subject", type=str, default=None, help="User subject identifier")
53
53
  @click.option("--creds-subject", type=str, default=None, help="Client credentials application subject")
54
+ @click.option("--email", type=str, default=None, help="User email for lookup")
54
55
  @click.pass_obj
55
- def get_assignment(cfg: common.CLIConfig, user_subject: str | None, creds_subject: str | None):
56
+ def get_assignment(cfg: common.CLIConfig, user_subject: str | None, creds_subject: str | None, email: str | None):
56
57
  """Get or list assignments.
57
58
 
58
- Without --user-subject or --creds-subject, lists all assignments.
59
+ Without --user-subject, --creds-subject, or --email, lists all assignments.
59
60
 
60
61
  Examples:
61
62
 
62
63
  $ flyte --org my-org get assignment
63
64
  $ flyte --org my-org get assignment --user-subject user-123
64
65
  $ flyte --org my-org get assignment --creds-subject app-456
66
+ $ flyte --org my-org get assignment --email jane@example.com
65
67
  """
66
68
  cfg.init(project="", domain="")
67
69
  console = common.get_console()
68
70
 
69
71
  try:
70
- if user_subject or creds_subject:
71
- assignment = Assignment.get(user_subject=user_subject, creds_subject=creds_subject)
72
+ if user_subject or creds_subject or email:
73
+ assignment = Assignment.get(user_subject=user_subject, creds_subject=creds_subject, email=email)
72
74
  console.print(common.format("Assignment", [assignment], "json"))
73
75
  else:
74
76
  assignments = list(Assignment.listall())
@@ -83,6 +85,7 @@ def get_assignment(cfg: common.CLIConfig, user_subject: str | None, creds_subjec
83
85
  @click.command("assignment")
84
86
  @click.option("--user-subject", type=str, default=None, help="User subject identifier")
85
87
  @click.option("--creds-subject", type=str, default=None, help="Client credentials application subject")
88
+ @click.option("--email", type=str, default=None, help="User email for lookup")
86
89
  @click.option("--policy", type=str, required=True, help="Policy name to unassign")
87
90
  @click.option("--yes", is_flag=True, help="Skip confirmation prompt")
88
91
  @click.pass_obj
@@ -90,30 +93,33 @@ def delete_assignment(
90
93
  cfg: common.CLIConfig,
91
94
  user_subject: str | None,
92
95
  creds_subject: str | None,
96
+ email: str | None,
93
97
  policy: str,
94
98
  yes: bool,
95
99
  ):
96
100
  """Unassign a policy from an identity.
97
101
 
98
- One of --user-subject or --creds-subject must be provided.
102
+ Exactly one of --user-subject, --creds-subject, or --email must be provided.
99
103
 
100
104
  Examples:
101
105
 
102
106
  $ flyte --org my-org delete assignment --user-subject user-123 --policy admin
103
107
  $ flyte --org my-org delete assignment --creds-subject app-456 --policy admin
108
+ $ flyte --org my-org delete assignment --email jane@example.com --policy admin
104
109
  """
105
110
  cfg.init(project="", domain="")
106
111
  console = common.get_console()
107
112
 
108
- if not user_subject and not creds_subject:
109
- raise click.ClickException("One of --user-subject or --creds-subject must be provided")
113
+ provided = sum(x is not None for x in (user_subject, creds_subject, email))
114
+ if provided != 1:
115
+ raise click.ClickException("Exactly one of --user-subject, --creds-subject, or --email must be provided")
110
116
 
111
- subject = user_subject or creds_subject
117
+ subject = user_subject or creds_subject or email
112
118
  if not yes:
113
119
  click.confirm(f"Are you sure you want to unassign policy '{policy}' from '{subject}'?", abort=True)
114
120
 
115
121
  try:
116
- Assignment.unassign(user_subject=user_subject, creds_subject=creds_subject, policy=policy)
122
+ Assignment.unassign(user_subject=user_subject, creds_subject=creds_subject, email=email, policy=policy)
117
123
  console.print(
118
124
  f"[green]✓[/green] Successfully unassigned policy [cyan]{policy}[/cyan] from [cyan]{subject}[/cyan]"
119
125
  )
@@ -0,0 +1,260 @@
1
+ """``flyte debug <run> [action]`` — relaunch an existing run with ssh-debug, then connect.
2
+
3
+ The single ssh-into-task entry point: it re-runs the given run with the ssh-debug
4
+ env injected (via `flyteplugins.union.debug`, built on `flyte.rerun`), waits for the
5
+ new run's ``:6060`` route, and prints the ready-to-paste ``~/.ssh/config`` block.
6
+
7
+ The relaunch uses a **deterministic** name derived from the source run, so re-running
8
+ ``flyte debug <run>`` reconnects to the live debug run instead of spawning a duplicate:
9
+ the backend's already-exists check short-circuits the relaunch and we just resolve the
10
+ route + (re)write the ssh-config.
11
+
12
+ Registered as a top-level ``debug`` command (the plugin loader won't auto-create a
13
+ missing verb group; a single command needs no group).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Optional
20
+
21
+ import rich_click as click
22
+
23
+ # Heavy imports (rich, remote objects, the SDK runner) are deferred into the command
24
+ # body so registering the entry point stays fast.
25
+
26
+ # Conservative cap for a derived run name (DNS-label-like: lowercase alphanumeric + hyphens).
27
+ _RUN_NAME_MAX = 63
28
+ _DEBUG_SUFFIX = "-dbg"
29
+
30
+
31
+ def _sanitize_run_name(source: str, *, reserve: int = 0) -> str:
32
+ """Coerce *source* into a valid run name (DNS-label-like, length-bounded).
33
+
34
+ *reserve* leaves room for a caller-appended suffix. The backend rejects user-given run
35
+ names starting with 'u', so a leading 'u' (and any hyphen it exposes) is dropped.
36
+ """
37
+ stem = re.sub(r"[^a-z0-9-]+", "-", source.lower()).strip("-")
38
+ if stem.startswith("u"):
39
+ stem = stem[1:].lstrip("-")
40
+ return stem[: _RUN_NAME_MAX - reserve] or "dbg"
41
+
42
+
43
+ def _debug_run_name(source: str) -> str:
44
+ """Deterministic debug-run name for a source run.
45
+
46
+ Re-running ``flyte debug <run>`` derives the same name, so the second invocation hits the
47
+ backend's already-exists check and we reconnect to the live debug run instead of spawning a
48
+ duplicate. The suffix keeps it distinct from the source run.
49
+ """
50
+ return _sanitize_run_name(source, reserve=len(_DEBUG_SUFFIX)) + _DEBUG_SUFFIX
51
+
52
+
53
+ @click.command("debug")
54
+ @click.argument("run_name", required=True)
55
+ @click.argument("action_name", required=False)
56
+ @click.option("--user", type=str, default="root", show_default=True, help="SSH login user inside the pod.")
57
+ @click.option(
58
+ "--identity-file",
59
+ "identity_file",
60
+ type=str,
61
+ default=None,
62
+ help="Private key to authenticate with. Defaults to the auto-managed "
63
+ "~/.flyte/ssh-debug/id_ed25519 (created for you; no ssh-keygen needed).",
64
+ )
65
+ @click.option(
66
+ "--name",
67
+ "name",
68
+ type=str,
69
+ default=None,
70
+ help="Name for this debug session — used as both the ~/.ssh/config Host alias and the remote "
71
+ "debug run name. Re-running with the same name reconnects to the live session; a new name "
72
+ "starts a fresh debug run (handy if the previous one died). "
73
+ "Defaults to a name derived from RUN_NAME.",
74
+ )
75
+ @click.option(
76
+ "--api-key/--no-api-key",
77
+ "api_key",
78
+ default=False,
79
+ help="Authenticate the tunnel with a dedicated, long-lived API key (created/reused "
80
+ "as `flyte-ssh-debug`) instead of your interactive session token. Survives re-logins "
81
+ "and won't expire mid-session.",
82
+ )
83
+ @click.option(
84
+ "--refresh-token",
85
+ "refresh_token",
86
+ is_flag=True,
87
+ help="Force a fresh Bearer instead of reusing the cached one (use if you hit auth errors).",
88
+ )
89
+ # TODO: add a `--recover` flag (recover from the run: reuse its succeeded actions) once the
90
+ # flyteidl2 RunSpec.recover field + backend support ship.
91
+ @click.option(
92
+ "--write-config",
93
+ "write_config",
94
+ is_flag=True,
95
+ help="Write the Host block into ~/.ssh/config (replacing any prior block for the same name).",
96
+ )
97
+ @click.option(
98
+ "--wait/--no-wait",
99
+ "wait",
100
+ default=True,
101
+ show_default=True,
102
+ help="Wait for the debug run to start and its ssh route to become ready, then print the "
103
+ "ssh-config. With --no-wait, relaunch and return immediately (don't block on the pod coming "
104
+ "up); re-run the same command later to attach once it's running.",
105
+ )
106
+ @click.option(
107
+ "--timeout",
108
+ type=float,
109
+ default=300.0,
110
+ show_default=True,
111
+ help="Seconds to wait for the debug route to become ready.",
112
+ )
113
+ @click.pass_obj
114
+ def debug(
115
+ cfg,
116
+ run_name: str,
117
+ action_name: Optional[str],
118
+ user: str,
119
+ identity_file: str,
120
+ name: Optional[str],
121
+ api_key: bool,
122
+ refresh_token: bool,
123
+ write_config: bool,
124
+ wait: bool,
125
+ timeout: float,
126
+ ) -> None:
127
+ """Relaunch RUN_NAME's task with ssh-debug enabled, then connect.
128
+
129
+ Only RUN_NAME is required; ACTION_NAME defaults to the root action ``a0``. The new run
130
+ re-uses the original inputs and comes up with sshd; this then prints its ssh-config.
131
+
132
+ Re-running ``flyte debug RUN_NAME`` reconnects to the same debug run (it isn't relaunched
133
+ twice) and refreshes the token in place — so it doubles as "just reattach".
134
+
135
+ Examples:
136
+
137
+ # Relaunch a prior run in debug mode and print its ssh-config
138
+ $ flyte debug my-run
139
+
140
+ # Named session (ssh Host alias + remote run name), written into ~/.ssh/config
141
+ $ flyte debug my-run --name my-dbg --api-key --write-config
142
+
143
+ # A new --name starts a fresh debug run (e.g. the previous one died)
144
+ $ flyte debug my-run --name my-dbg2
145
+
146
+ # Fire-and-forget: relaunch and return without waiting for the pod to come up
147
+ $ flyte debug my-run --no-wait
148
+
149
+ # Then connect (or VS Code -> Remote-SSH -> <name>)
150
+ $ ssh my-dbg
151
+ """
152
+ import flyte.errors
153
+ from flyte.cli import _common as common
154
+
155
+ from flyteplugins.union import debug as debug_run
156
+ from flyteplugins.union.remote import SSHDebug
157
+ from flyteplugins.union.remote._ssh_debug import DebugRunTerminalError
158
+
159
+ action_name = action_name or "a0"
160
+ cfg.init()
161
+
162
+ console = common.get_console()
163
+ # --name (when given) is both the remote run name and the ssh Host alias; otherwise derive a
164
+ # deterministic run name from RUN_NAME (so re-running reconnects) and use the default host alias.
165
+ debug_name = _sanitize_run_name(name) if name else _debug_run_name(run_name)
166
+ host_alias = name or "flyte-debug"
167
+ # Connect to the debug run regardless of whether we launched it now or it was already up.
168
+ target = debug_name
169
+
170
+ with console.status(f"[bold]Relaunching[/] {run_name}/{action_name} with ssh-debug…", spinner="dots"):
171
+ try:
172
+ # Record the chosen host alias on the run (ssh-host-name); the alias used to build the
173
+ # ssh-config below is still threaded directly via host_alias (no read-back).
174
+ new_run = debug_run(run_name, action_name=action_name, name=debug_name, ssh_host_name=name)
175
+ target = new_run.name
176
+ console.print(f"[green]Relaunched[/] as [bold]{new_run.name}[/] {new_run.url}")
177
+ except flyte.errors.RuntimeUserError as e:
178
+ # A prior `flyte debug {run_name}` already launched this debug run — reconnect to it
179
+ # instead of failing. (Any other user error still surfaces.)
180
+ if getattr(e, "code", None) != "RunAlreadyExistsError":
181
+ raise
182
+ console.print(f"[yellow]Debug run[/] [bold]{debug_name}[/] already exists — reconnecting.")
183
+
184
+ if not wait:
185
+ # --no-wait: don't block on the pod booting + sshd coming up. The same command, re-run
186
+ # once the run is going, reconnects (it won't relaunch) and prints the ssh-config.
187
+ reattach = f"flyte debug {run_name}" + (f" --name {name}" if name else "")
188
+ console.print(
189
+ f"[dim]--no-wait: not waiting for the ssh route on [bold]{target}[/]. "
190
+ f"Once it's running, re-run [bold]{reattach}[/] to attach (reconnects, won't relaunch), "
191
+ f"then [bold]ssh {host_alias}[/].[/dim]"
192
+ )
193
+ return
194
+
195
+ with console.status(f"[bold]Resolving[/] ssh route for {target}/a0…", spinner="dots"):
196
+ try:
197
+ info = SSHDebug.connect(
198
+ target,
199
+ "a0",
200
+ user=user,
201
+ identity_file=identity_file,
202
+ host_alias=host_alias,
203
+ use_api_key=api_key,
204
+ refresh_token=refresh_token,
205
+ timeout=timeout,
206
+ )
207
+ except (TimeoutError, PermissionError, DebugRunTerminalError) as e:
208
+ raise click.ClickException(str(e)) from e
209
+
210
+ _emit(console, info, run_name=target, action_name="a0", write_config=write_config)
211
+
212
+
213
+ def _emit(console, info, *, run_name, action_name, write_config):
214
+ """Write the Host block (optionally) and print the connect panel."""
215
+ import os
216
+ from pathlib import Path
217
+
218
+ from rich.panel import Panel
219
+ from rich.syntax import Syntax
220
+
221
+ if write_config:
222
+ ssh_config_path = Path(os.path.expanduser("~/.ssh/config"))
223
+ ssh_config_path.parent.mkdir(parents=True, exist_ok=True)
224
+ existing = ssh_config_path.read_text() if ssh_config_path.exists() else ""
225
+ # Drop any prior `Host <alias>` block (header + its indented body) so
226
+ # re-running replaces stale config instead of shadowing it with a dup
227
+ # (ssh honors the first matching Host).
228
+ kept, skipping = [], False
229
+ for line in existing.splitlines():
230
+ if line.strip().startswith("Host ") and line.split()[1:] == [info.host_alias]:
231
+ skipping = True
232
+ continue
233
+ if skipping:
234
+ # Block body is blank or indented; ends at the next non-indented, non-blank line.
235
+ if line and not line[0].isspace():
236
+ skipping = False
237
+ else:
238
+ continue
239
+ kept.append(line)
240
+ new_text = "\n".join(kept).rstrip() + "\n\n" + info.ssh_config
241
+ ssh_config_path.write_text(new_text)
242
+ console.print(f"[green]Wrote[/] Host [bold]{info.host_alias}[/] to {ssh_config_path}")
243
+
244
+ console.print(
245
+ Panel.fit(
246
+ Syntax(info.ssh_config, "sshconfig", theme="ansi_dark", background_color="default"),
247
+ title=f"[bold]ssh -> {run_name}/{action_name}[/bold]",
248
+ subtitle=f"then: ssh {info.host_alias} (or VS Code Remote-SSH -> {info.host_alias})",
249
+ border_style="cyan",
250
+ )
251
+ )
252
+ if info.headers_file:
253
+ console.print(f"[dim]Bearer header written to {info.headers_file} (0600)[/dim]")
254
+ # Prominent next step: the literal command to run.
255
+ console.print(f"\n[bold green]➜ Connect now:[/] ssh {info.host_alias}")
256
+ console.print(
257
+ "[dim]Token expired / 401 on reconnect? Just re-run the same `flyte debug` command "
258
+ "(reconnects + refreshes in place — no --write-config needed). "
259
+ "For multi-day sessions use --api-key.[/dim]"
260
+ )
@@ -53,6 +53,19 @@ class VolumeUnmountError(VolumeError):
53
53
  _code = "VolumeUnmountFailed"
54
54
 
55
55
 
56
+ class VolumeUnmountUnsupportedError(VolumeUnmountError):
57
+ """The runtime cannot unmount the FUSE client at all because ``fusermount``
58
+ can't reach its mount table (``/etc/mtab`` is missing, read-only, or
59
+ unwritable). Unlike a transient :class:`VolumeUnmountError` (EBUSY), retrying
60
+ won't help — the image is missing the ``/etc/mtab`` → ``/proc/self/mounts``
61
+ symlink most base images carry. The terminal seal treats this as recoverable:
62
+ it falls back to a keep-alive (live) commit, leaving the mount in place. A
63
+ kind of :class:`VolumeUnmountError`, so ``except VolumeUnmountError`` catches
64
+ it too."""
65
+
66
+ _code = "VolumeUnmountUnsupported"
67
+
68
+
56
69
  class VolumeCommitError(VolumeError):
57
70
  """A keep-alive checkpoint could not be made durable: the writeback staging
58
71
  queue did not finish uploading to object storage (it timed out, or its