flyteplugins-union 0.5.0__tar.gz → 0.5.2__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.5.0/src/flyteplugins_union.egg-info → flyteplugins_union-0.5.2}/PKG-INFO +1 -1
  2. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/licenses/BSL.txt +1 -1
  3. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/role.py +13 -7
  4. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_base_volume.py +50 -1
  5. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/_backend.py +14 -0
  6. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/_redis.py +98 -22
  7. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/_sqlite.py +12 -0
  8. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_role.py +20 -12
  9. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2/src/flyteplugins_union.egg-info}/PKG-INFO +1 -1
  10. flyteplugins_union-0.5.2/src/flyteplugins_union.egg-info/scm_version.json +8 -0
  11. flyteplugins_union-0.5.0/src/flyteplugins_union.egg-info/scm_version.json +0 -8
  12. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/LICENSE +0 -0
  13. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/README.md +0 -0
  14. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/README_PYPI.md +0 -0
  15. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/licenses/APL.txt +0 -0
  16. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/pyproject.toml +0 -0
  17. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/setup.cfg +0 -0
  18. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/__init__.py +0 -0
  19. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/_debug.py +0 -0
  20. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/__init__.py +0 -0
  21. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/_tui/__init__.py +0 -0
  22. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/_tui/_volume_explore.py +0 -0
  23. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/_volume_index.py +0 -0
  24. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/_volume_session.py +0 -0
  25. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/api_key.py +0 -0
  26. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/assignment.py +0 -0
  27. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/cluster.py +0 -0
  28. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/cluster_pool.py +0 -0
  29. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/debug.py +0 -0
  30. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/member.py +0 -0
  31. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/policy.py +0 -0
  32. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/queue.py +0 -0
  33. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/user.py +0 -0
  34. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/cli/volume.py +0 -0
  35. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/errors.py +0 -0
  36. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/__init__.py +0 -0
  37. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/authorizer_connect.py +0 -0
  38. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/authorizer_pb2.py +0 -0
  39. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/authorizer_pb2.pyi +0 -0
  40. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/definition_pb2.py +0 -0
  41. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/definition_pb2.pyi +0 -0
  42. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/payload_pb2.py +0 -0
  43. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/authorizer/payload_pb2.pyi +0 -0
  44. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/cluster_connect.py +0 -0
  45. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/cluster_pb2.py +0 -0
  46. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/cluster_pb2.pyi +0 -0
  47. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/definition_pb2.py +0 -0
  48. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/definition_pb2.pyi +0 -0
  49. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/payload_pb2.py +0 -0
  50. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/cluster/payload_pb2.pyi +0 -0
  51. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/clusterpool/clusterpool_connect.py +0 -0
  52. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/clusterpool/clusterpool_pb2.py +0 -0
  53. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/clusterpool/clusterpool_pb2.pyi +0 -0
  54. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/clusterpool/payload_pb2.py +0 -0
  55. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/clusterpool/payload_pb2.pyi +0 -0
  56. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/authorization_pb2.py +0 -0
  57. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/authorization_pb2.pyi +0 -0
  58. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/cluster_pb2.py +0 -0
  59. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/cluster_pb2.pyi +0 -0
  60. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/deployment_pb2.py +0 -0
  61. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/deployment_pb2.pyi +0 -0
  62. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/identifier_pb2.py +0 -0
  63. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/identifier_pb2.pyi +0 -0
  64. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/identity_pb2.py +0 -0
  65. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/identity_pb2.pyi +0 -0
  66. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/list_pb2.py +0 -0
  67. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/list_pb2.pyi +0 -0
  68. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/policy_pb2.py +0 -0
  69. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/policy_pb2.pyi +0 -0
  70. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/role_pb2.py +0 -0
  71. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/common/role_pb2.pyi +0 -0
  72. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_definition_pb2.py +0 -0
  73. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_definition_pb2.pyi +0 -0
  74. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_payload_pb2.py +0 -0
  75. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_payload_pb2.pyi +0 -0
  76. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_service_connect.py +0 -0
  77. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_service_pb2.py +0 -0
  78. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/app_service_pb2.pyi +0 -0
  79. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/enums_pb2.py +0 -0
  80. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/enums_pb2.pyi +0 -0
  81. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/member_payload_pb2.py +0 -0
  82. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/member_payload_pb2.pyi +0 -0
  83. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/member_service_connect.py +0 -0
  84. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/member_service_pb2.py +0 -0
  85. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/member_service_pb2.pyi +0 -0
  86. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/policy_payload_pb2.py +0 -0
  87. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/policy_payload_pb2.pyi +0 -0
  88. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/policy_service_connect.py +0 -0
  89. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/policy_service_pb2.py +0 -0
  90. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/policy_service_pb2.pyi +0 -0
  91. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/role_payload_pb2.py +0 -0
  92. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/role_payload_pb2.pyi +0 -0
  93. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/role_service_connect.py +0 -0
  94. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/role_service_pb2.py +0 -0
  95. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/role_service_pb2.pyi +0 -0
  96. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/user_payload_pb2.py +0 -0
  97. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/user_payload_pb2.pyi +0 -0
  98. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/user_service_connect.py +0 -0
  99. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/user_service_pb2.py +0 -0
  100. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/identity/user_service_pb2.pyi +0 -0
  101. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/queue/queue_connect.py +0 -0
  102. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/queue/queue_pb2.py +0 -0
  103. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/queue/queue_pb2.pyi +0 -0
  104. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/validate/__init__.py +0 -0
  105. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/validate/validate/__init__.py +0 -0
  106. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/internal/validate/validate/validate_pb2.py +0 -0
  107. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/__init__.py +0 -0
  108. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/__init__.py +0 -0
  109. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/__init__.py +0 -0
  110. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/_common.py +0 -0
  111. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/_metadata.py +0 -0
  112. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/_juicefs/bin/.gitkeep +0 -0
  113. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_internal/backend.py +0 -0
  114. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_ro_volume.py +0 -0
  115. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_rw_volume.py +0 -0
  116. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/io/_volume_transformer.py +0 -0
  117. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/__init__.py +0 -0
  118. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_api_key.py +0 -0
  119. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_assignment.py +0 -0
  120. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_cluster.py +0 -0
  121. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_cluster_pool.py +0 -0
  122. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_member.py +0 -0
  123. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_policy.py +0 -0
  124. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_queue.py +0 -0
  125. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_ssh_debug.py +0 -0
  126. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_user.py +0 -0
  127. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/remote/_volume_explore.py +0 -0
  128. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/utils/__init__.py +0 -0
  129. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/utils/auth.py +0 -0
  130. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/utils/image.py +0 -0
  131. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins/union/ws_proxy.py +0 -0
  132. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins_union.egg-info/SOURCES.txt +0 -0
  133. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins_union.egg-info/dependency_links.txt +0 -0
  134. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins_union.egg-info/entry_points.txt +0 -0
  135. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins_union.egg-info/requires.txt +0 -0
  136. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/src/flyteplugins_union.egg-info/scm_file_list.json +0 -0
  137. {flyteplugins_union-0.5.0 → flyteplugins_union-0.5.2}/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.5.0
3
+ Version: 0.5.2
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/
@@ -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-29
20
+ Change Date: 2030-07-13
21
21
 
22
22
  Change License: Apache License, Version 2.0
23
23
 
@@ -21,10 +21,13 @@ def _new_role_yaml(name: str, description: str = "") -> str:
21
21
 
22
22
 
23
23
  def _role_to_yaml(role: Role) -> str:
24
- """Serialize a Role to YAML for interactive editing."""
25
- data = {"name": role.name, "description": role.description, "actions": role.actions}
24
+ """Serialize a Role to YAML for interactive editing.
25
+
26
+ Only actions are editable; the role name and description cannot be changed.
27
+ """
28
+ data = {"name": role.name, "actions": role.actions}
26
29
  body = _yaml.safe_dump(data, default_flow_style=False, sort_keys=False)
27
- comment = "# Valid actions:\n"
30
+ comment = "# Only 'actions' can be edited. Valid actions:\n"
28
31
  for a in _ALL_ACTIONS:
29
32
  comment += f"# - {a}\n"
30
33
  return comment + "\n" + body
@@ -141,9 +144,10 @@ def delete_role(cfg: common.CLIConfig, name: str, yes: bool):
141
144
  @click.argument("name", type=str)
142
145
  @click.pass_obj
143
146
  def update_role(cfg: common.CLIConfig, name: str):
144
- """Update a role interactively.
147
+ """Update a role's actions interactively.
145
148
 
146
149
  Opens the role in your $EDITOR as YAML. Save and close to apply changes.
150
+ Only actions can be edited; the role name and description cannot be changed.
147
151
 
148
152
  Examples:
149
153
 
@@ -161,11 +165,13 @@ def update_role(cfg: common.CLIConfig, name: str):
161
165
 
162
166
  def _apply(edited_yaml):
163
167
  data = _yaml.safe_load(edited_yaml)
164
- new_description = data.get("description", "")
165
168
  new_actions = sorted(data.get("actions") or [])
166
- if new_description == role.description and new_actions == sorted(role.actions):
169
+ if new_actions == sorted(role.actions):
167
170
  return "no_changes"
168
- Role.update(name, description=new_description, actions=data.get("actions"))
171
+ Role.update(name, actions=data.get("actions") or [])
172
+ # edit_with_retry treats None as "cancelled"; return a marker so the
173
+ # success message below is printed.
174
+ return "updated"
169
175
 
170
176
  result = edit_with_retry(yaml_text, _apply, console=console, noun="role")
171
177
  if result == "no_changes":
@@ -56,6 +56,7 @@ automatically at task return) also unmounts.
56
56
  from __future__ import annotations
57
57
 
58
58
  import asyncio
59
+ import functools
59
60
  import os
60
61
  import re
61
62
  import secrets
@@ -172,6 +173,30 @@ def _ensure_etc_mtab() -> None:
172
173
  _VOLUME_BACKENDS: dict[str, _VolumeBackend] = {"juicefs": JuiceFSVolumeBackend()}
173
174
 
174
175
 
176
+ def _lifecycle_serialized(method):
177
+ """Serialize lifecycle ops (mount / fork / commit) per metadata store.
178
+
179
+ A background checkpoint loop (:meth:`RWVolume.commit` on a timer) can
180
+ otherwise interleave with a concurrent :meth:`fork` or terminal
181
+ :meth:`RWVolume.finalize` on the same volume: fork's live path unmounts
182
+ and remounts the store mid-snapshot, so an unserialized checkpoint can
183
+ flush a store that fork just tore down, snapshot a half-materialized
184
+ index, or double-register the mount daemon. All entry points funnel
185
+ through one ``asyncio.Lock`` keyed by resolved ``meta_dir`` (the store
186
+ identity; mount paths alias it 1:1 today). Internal helpers stay
187
+ undecorated — only entry points take the lock, so the
188
+ ``_commit_unmount`` → keep-alive fallback can't self-deadlock.
189
+ """
190
+
191
+ @functools.wraps(method)
192
+ async def wrapper(self, *args, **kwargs):
193
+ meta_dir = kwargs.get("meta_dir") or self._active_meta_dir or self._default_meta_dir()
194
+ async with type(self)._lifecycle_lock(meta_dir):
195
+ return await method(self, *args, **kwargs)
196
+
197
+ return wrapper
198
+
199
+
175
200
  class ActionRef(BaseModel):
176
201
  """Provenance: the action (one task execution within a run) that
177
202
  produced a particular :class:`Volume` version, plus the output slot it
@@ -312,6 +337,13 @@ class Volume(BaseModel):
312
337
  # to decide whether to use the live flush-and-upload path or the cold
313
338
  # File.copy_to path. Populated by mount(), cleared by commit().
314
339
  _live_meta: ClassVar[Set[str]] = set()
340
+ # One lock per metadata store, taken by every lifecycle entry point via
341
+ # ``@_lifecycle_serialized``. Never handed out — see that decorator.
342
+ _lifecycle_locks: ClassVar[Dict[str, asyncio.Lock]] = {}
343
+
344
+ @classmethod
345
+ def _lifecycle_lock(cls, meta_dir: str) -> asyncio.Lock:
346
+ return cls._lifecycle_locks.setdefault(meta_dir, asyncio.Lock())
315
347
 
316
348
  # Per-instance: where THIS Volume was last mounted by mount(), so the
317
349
  # type transformer can auto-commit on task return without the caller
@@ -435,6 +467,7 @@ class Volume(BaseModel):
435
467
  base = cls.empty(name, bucket, storage=storage, region=region, metadata_store_type=metadata_store_type)
436
468
  return _coerce_class(base, RWVolume)
437
469
 
470
+ @_lifecycle_serialized
438
471
  async def mount(
439
472
  self,
440
473
  *,
@@ -774,6 +807,7 @@ class Volume(BaseModel):
774
807
  message=message,
775
808
  )
776
809
 
810
+ @_lifecycle_serialized
777
811
  async def _commit_unmount(
778
812
  self,
779
813
  *,
@@ -840,7 +874,8 @@ class Volume(BaseModel):
840
874
  "add `RUN ln -sf /proc/self/mounts /etc/mtab` to the task image.",
841
875
  e,
842
876
  )
843
- return await self._commit_keepalive(mount_path=mount_path, meta_dir=meta_dir, message=message)
877
+ # Direct impl call: we already hold this store's lifecycle lock.
878
+ return await self._commit_keepalive_impl(mount_path=mount_path, meta_dir=meta_dir, message=message)
844
879
  new_index, index_bytes, metadata_path = await self._snapshot_and_upload_index(
845
880
  meta_dir=meta_dir, tmp_prefix="vol-commit-", flush_live=False
846
881
  )
@@ -1016,6 +1051,7 @@ class Volume(BaseModel):
1016
1051
  except OSError:
1017
1052
  pass
1018
1053
 
1054
+ @_lifecycle_serialized
1019
1055
  async def fork(
1020
1056
  self,
1021
1057
  name: str,
@@ -1195,12 +1231,25 @@ class Volume(BaseModel):
1195
1231
 
1196
1232
  return config
1197
1233
 
1234
+ @_lifecycle_serialized
1198
1235
  async def _commit_keepalive(
1199
1236
  self,
1200
1237
  *,
1201
1238
  mount_path: Optional[str] = None,
1202
1239
  meta_dir: Optional[str] = None,
1203
1240
  message: Optional[str] = None,
1241
+ ) -> "Volume":
1242
+ """Serialized entry point for keep-alive commits (see
1243
+ :func:`_lifecycle_serialized`); :meth:`_commit_unmount` calls the impl
1244
+ directly because it already holds the store's lock."""
1245
+ return await self._commit_keepalive_impl(mount_path=mount_path, meta_dir=meta_dir, message=message)
1246
+
1247
+ async def _commit_keepalive_impl(
1248
+ self,
1249
+ *,
1250
+ mount_path: Optional[str] = None,
1251
+ meta_dir: Optional[str] = None,
1252
+ message: Optional[str] = None,
1204
1253
  ) -> "Volume":
1205
1254
  """Drain writeback, then snapshot + publish the live index — without
1206
1255
  unmounting. The keep-alive counterpart to :meth:`_commit_unmount`,
@@ -318,6 +318,20 @@ class JuiceFSVolumeBackend:
318
318
  cmd = [
319
319
  self.client_binary,
320
320
  "mount",
321
+ # Branches share one bucket but each has a private metadata DB, so a
322
+ # client's slice refcounts can't see sibling/parent references. Any
323
+ # client-side object deletion (slice compaction — fired inline on
324
+ # the read/write path, not bgjob-gated — file purge, trash expiry)
325
+ # may therefore destroy chunks other branches still reference.
326
+ # --max-deletes 0 no-ops the single gate all those paths funnel
327
+ # through (meta.deleteSlice); compaction still rewrites metadata
328
+ # and writes the merged slice, it just never deletes old objects.
329
+ # --no-bgjob additionally stops the hourly cleanup sweeps and
330
+ # shared-bucket meta backups. Garbage is reclaimed by an external
331
+ # reaper that unions live-branch references; never `juicefs gc`.
332
+ "--max-deletes",
333
+ "0",
334
+ "--no-bgjob",
321
335
  "--cache-dir",
322
336
  config.cache_dir,
323
337
  "--max-uploads",
@@ -31,7 +31,9 @@ def _free_tcp_port() -> int:
31
31
  def _redis_server_cmd(port: int, dirpath: str, dbfilename: str) -> list[str]:
32
32
  """Argv for an in-process ``redis-server`` bound to localhost on ``port``,
33
33
  persisting ``dbfilename`` under ``dirpath``. RDB-only (no AOF), no periodic
34
- save — we drive persistence explicitly via ``SAVE``.
34
+ save — we drive persistence explicitly via ``BGSAVE`` (see
35
+ :func:`_bgsave_and_wait`, which relies on ours being the only dump in
36
+ flight).
35
37
  """
36
38
  return [
37
39
  "redis-server",
@@ -82,7 +84,21 @@ async def _redis_cli(port: int, *args: str, check: bool = False, timeout: float
82
84
  return rc, stdout
83
85
 
84
86
 
85
- async def _wait_redis_ready(proc: subprocess.Popen, port: int, timeout: float) -> None:
87
+ def _tail_log(log_path: Optional[Path], max_bytes: int = 4096) -> str:
88
+ """Best-effort read of the last ``max_bytes`` of a daemon log file."""
89
+ if log_path is None:
90
+ return ""
91
+ try:
92
+ with open(log_path, "rb") as f:
93
+ f.seek(0, 2)
94
+ size = f.tell()
95
+ f.seek(max(0, size - max_bytes))
96
+ return f.read().decode(errors="replace")
97
+ except OSError:
98
+ return ""
99
+
100
+
101
+ async def _wait_redis_ready(proc: subprocess.Popen, port: int, timeout: float, log_path: Optional[Path] = None) -> None:
86
102
  """Poll ``PING`` until the server replies ``PONG`` (or ``timeout``).
87
103
 
88
104
  Raises :class:`RuntimeError` if the server exits before becoming ready and
@@ -92,9 +108,9 @@ async def _wait_redis_ready(proc: subprocess.Popen, port: int, timeout: float) -
92
108
  deadline = asyncio.get_running_loop().time() + timeout
93
109
  while True:
94
110
  if proc.poll() is not None:
95
- out = proc.stdout.read().decode(errors="replace") if proc.stdout else ""
96
111
  raise RuntimeSystemError(
97
- "RedisStartFailed", f"redis-server exited prematurely (rc={proc.returncode}): {out}"
112
+ "RedisStartFailed",
113
+ f"redis-server exited prematurely (rc={proc.returncode}): {_tail_log(log_path)}",
98
114
  )
99
115
  rc, stdout = await _redis_cli(port, "ping", timeout=5.0)
100
116
  if rc == 0 and "PONG" in stdout:
@@ -125,14 +141,24 @@ class RedisMetadataStore:
125
141
  async def start(self, meta_dir: str, timeout: float = 30.0) -> _MetadataStoreHandle:
126
142
  port = _free_tcp_port()
127
143
  _logger.info("[Volume.mount] starting redis-server in %s", meta_dir)
128
- proc = subprocess.Popen( # noqa: ASYNC220 - long-lived daemon held in a class-level dict
129
- _redis_server_cmd(port, meta_dir, _INDEX_FILENAME),
130
- stdout=subprocess.PIPE,
131
- stderr=subprocess.STDOUT,
132
- start_new_session=True,
133
- )
134
- await _wait_redis_ready(proc, port, timeout)
135
- _logger.info("[Volume.mount] redis-server ready on port %d", port)
144
+ # Route the daemon's logs to a file, never a captured pipe: redis-server
145
+ # logs for its whole lifetime (background saves etc.), and a PIPE that
146
+ # nothing drains fills its ~64 KiB kernel buffer — redis then blocks
147
+ # inside its log write, every metadata op stalls behind it, and the
148
+ # whole mount wedges. Same failure mode (and same fix) as the mount
149
+ # daemon's stdio in ``_start_mount``; a file also preserves premature-
150
+ # exit diagnostics, which inherited stdio would scatter into the
151
+ # container log.
152
+ log_path = Path(meta_dir) / "redis-server.log"
153
+ with open(log_path, "ab") as logf: # noqa: ASYNC230 - local append-open, microseconds
154
+ proc = subprocess.Popen( # noqa: ASYNC220 - long-lived daemon held in a class-level dict
155
+ _redis_server_cmd(port, meta_dir, _INDEX_FILENAME),
156
+ stdout=logf,
157
+ stderr=subprocess.STDOUT,
158
+ start_new_session=True,
159
+ )
160
+ await _wait_redis_ready(proc, port, timeout, log_path)
161
+ _logger.info("[Volume.mount] redis-server ready on port %d (logs: %s)", port, log_path)
136
162
  return _MetadataStoreHandle(proc=proc, port=port)
137
163
 
138
164
  async def stop(self, handle: Optional[_MetadataStoreHandle], timeout: float) -> None:
@@ -141,7 +167,7 @@ class RedisMetadataStore:
141
167
  """
142
168
  if handle is None:
143
169
  return
144
- # SHUTDOWN NOSAVE — we drove persistence ourselves via SAVE. redis-cli
170
+ # SHUTDOWN NOSAVE — we drove persistence ourselves via BGSAVE. redis-cli
145
171
  # may return non-zero as the connection drops mid-shutdown; that's
146
172
  # expected, so this call is unchecked. Never let a hung/failed shutdown
147
173
  # leave the daemon unreaped — fall through to the wait + kill below.
@@ -157,7 +183,7 @@ class RedisMetadataStore:
157
183
  async def flush(self, meta_dir: str, handle: Optional[_MetadataStoreHandle] = None) -> None:
158
184
  if handle is None:
159
185
  raise RuntimeError("RedisMetadataStore.flush requires a live handle from start().")
160
- await _redis_cli(handle.port, "SAVE", check=True)
186
+ await _bgsave_and_wait(handle.port)
161
187
 
162
188
  async def snapshot(self, meta_dir: str, tmp_prefix: str) -> str:
163
189
  src = Path(meta_dir) / _INDEX_FILENAME
@@ -176,6 +202,49 @@ class RedisMetadataStore:
176
202
  return await _disjoint_counters_redis(snapshot_path, offset)
177
203
 
178
204
 
205
+ async def _bgsave_and_wait(port: int, timeout: float = 600.0, poll_interval: float = 0.2) -> None:
206
+ """Persist the dataset to the RDB via ``BGSAVE`` and wait for completion.
207
+
208
+ Replaces blocking ``SAVE``, which had two failure modes: (a) it stalls the
209
+ (single-threaded) server for the whole dump, so every metadata op — and
210
+ therefore every FUSE op on a live mount — hangs during a keep-alive
211
+ commit; (b) ``redis-cli SAVE`` ran under the default 10s client timeout,
212
+ so a dump that took longer killed the cli, failed the flush, and aborted
213
+ the commit spuriously while the server kept SAVEing.
214
+
215
+ ``BGSAVE`` forks; the parent keeps serving. Our servers run with
216
+ ``--save ""`` (no periodic saves), so the only BGSAVE in flight is ours:
217
+ completion is "``rdb_bgsave_in_progress`` back to 0", and correctness is
218
+ ``rdb_last_bgsave_status:ok``. If a BGSAVE is already running (commit
219
+ racing a previous flush), the initial ``BGSAVE`` command reports the
220
+ schedule error but the poll below still tracks the in-flight dump; we
221
+ re-issue once it clears rather than fail.
222
+ """
223
+ loop = asyncio.get_running_loop()
224
+ deadline = loop.time() + timeout
225
+ rc, out = await _redis_cli(port, "BGSAVE")
226
+ already_running = rc != 0 or "in progress" in out.lower()
227
+ while True:
228
+ _, info = await _redis_cli(port, "INFO", "persistence", check=True)
229
+ fields = dict(
230
+ line.split(":", 1) for line in info.splitlines() if ":" in line and not line.startswith("#")
231
+ )
232
+ if fields.get("rdb_bgsave_in_progress", "").strip() == "0":
233
+ if already_running:
234
+ # The dump we watched finish was a racing predecessor's; run
235
+ # our own so this flush's writes are guaranteed on disk.
236
+ already_running = False
237
+ await _redis_cli(port, "BGSAVE", check=True)
238
+ continue
239
+ status = fields.get("rdb_last_bgsave_status", "").strip()
240
+ if status != "ok":
241
+ raise RuntimeSystemError("RedisBgsaveFailed", f"BGSAVE finished with status {status!r}")
242
+ return
243
+ if loop.time() >= deadline:
244
+ raise RuntimeSystemError("RedisBgsaveTimeout", f"BGSAVE did not complete within {timeout}s")
245
+ await asyncio.sleep(poll_interval)
246
+
247
+
179
248
  async def _disjoint_counters_redis(rdb_path: str, offset: int, timeout: float = 30.0) -> int:
180
249
  if offset <= 0:
181
250
  raise ValueError(f"offset must be positive, got {offset}")
@@ -184,14 +253,17 @@ async def _disjoint_counters_redis(rdb_path: str, offset: int, timeout: float =
184
253
  raise RuntimeError(f"{rdb_path} does not exist")
185
254
 
186
255
  port = _free_tcp_port()
187
- proc = subprocess.Popen( # noqa: ASYNC220 - ephemeral daemon, reaped in finally
188
- _redis_server_cmd(port, str(rdb.parent), rdb.name),
189
- stdout=subprocess.PIPE,
190
- stderr=subprocess.STDOUT,
191
- start_new_session=True,
192
- )
256
+ # File-backed logs for the same no-undrained-pipe reason as ``start()``.
257
+ log_path = rdb.parent / f"{rdb.name}.redis-fork.log"
258
+ with open(log_path, "ab") as logf: # noqa: ASYNC230 - local append-open, microseconds
259
+ proc = subprocess.Popen( # noqa: ASYNC220 - ephemeral daemon, reaped in finally
260
+ _redis_server_cmd(port, str(rdb.parent), rdb.name),
261
+ stdout=logf,
262
+ stderr=subprocess.STDOUT,
263
+ start_new_session=True,
264
+ )
193
265
  try:
194
- await _wait_redis_ready(proc, port, timeout)
266
+ await _wait_redis_ready(proc, port, timeout, log_path)
195
267
 
196
268
  current_max = 0
197
269
  for name in _FORK_COUNTER_NAMES:
@@ -208,7 +280,7 @@ async def _disjoint_counters_redis(rdb_path: str, offset: int, timeout: float =
208
280
  for name in _FORK_COUNTER_NAMES:
209
281
  await _redis_cli(port, "INCRBY", name, str(applied), check=True)
210
282
 
211
- await _redis_cli(port, "SAVE", check=True)
283
+ await _bgsave_and_wait(port, timeout=timeout)
212
284
  finally:
213
285
  # Best-effort shutdown — never let cleanup mask the real error.
214
286
  try:
@@ -218,4 +290,8 @@ async def _disjoint_counters_redis(rdb_path: str, offset: int, timeout: float =
218
290
  if not await _await_proc_exit(proc, 5):
219
291
  proc.kill()
220
292
  await _await_proc_exit(proc, 5)
293
+ try:
294
+ log_path.unlink()
295
+ except OSError:
296
+ pass
221
297
  return applied
@@ -58,6 +58,18 @@ class SqliteMetadataStore:
58
58
  # caller runs it off the loop. Kept sync so the sync CLI path can call
59
59
  # it directly without an event loop.
60
60
  dst = Path(meta_dir) / _INDEX_FILENAME
61
+ # Scrub sidecars from any previous incarnation of this meta_dir (a
62
+ # remount after commit, or a task retry reusing the pod). SQLite pairs
63
+ # a WAL/SHM with its main file via a header salt, but a leftover
64
+ # ``-wal`` written by an *older* database at this same path can pass
65
+ # that check and get replayed over the fresh snapshot, silently mixing
66
+ # two versions' metadata. The snapshot itself is complete (backup API
67
+ # folds the WAL in), so stale sidecars are never legitimate here.
68
+ for sidecar in (dst, Path(str(dst) + "-wal"), Path(str(dst) + "-shm")):
69
+ try:
70
+ sidecar.unlink()
71
+ except FileNotFoundError:
72
+ pass
61
73
  shutil.move(snapshot_path, str(dst))
62
74
 
63
75
  async def disjoint_fork_counters(self, snapshot_path: str, offset: int) -> int:
@@ -13,11 +13,12 @@ from flyteplugins.union.internal.common.identifier_pb2 import RoleIdentifier
13
13
  from flyteplugins.union.internal.common.role_pb2 import ROLE_TYPE_CUSTOM, RoleSpec, RoleType
14
14
  from flyteplugins.union.internal.common.role_pb2 import Role as RolePb2
15
15
  from flyteplugins.union.internal.identity.role_payload_pb2 import (
16
+ CreateRoleActionRequest,
16
17
  CreateRoleRequest,
18
+ DeleteRoleActionRequest,
17
19
  DeleteRoleRequest,
18
20
  GetRoleRequest,
19
21
  ListRolesRequest,
20
- UpdateRoleRequest,
21
22
  )
22
23
  from flyteplugins.union.internal.identity.role_service_connect import RoleServiceClient
23
24
 
@@ -112,22 +113,29 @@ class Role(ToJSONMixin):
112
113
 
113
114
  @syncify
114
115
  @classmethod
115
- async def update(cls, name: str, *, description: str = "", actions: list[str] | None = None) -> None:
116
- """Update a role."""
116
+ async def update(cls, name: str, *, actions: list[str]) -> None:
117
+ """Update a role so its actions match exactly the given list.
118
+
119
+ The UpdateRole RPC is deprecated and rejected server-side, so the
120
+ current actions are diffed against the desired ones and each delta is
121
+ applied via CreateRoleAction/DeleteRoleAction. The role name and
122
+ description cannot be changed.
123
+ """
117
124
  ensure_client()
118
125
  client = get_client()
119
126
  org = get_init_config().org
120
127
  svc = RoleServiceClient(**client.session_config.connect_kwargs())
121
128
 
122
- resolved = [Action.Value(_resolve_action_name(a)) for a in (actions or [])]
123
- role = RolePb2(
124
- id=RoleIdentifier(organization=org, name=name),
125
- role_spec=RoleSpec(description=description),
126
- role_type=ROLE_TYPE_CUSTOM,
127
- actions=resolved,
128
- )
129
- request = UpdateRoleRequest(role=role)
130
- await svc.update_role(request)
129
+ role_id = RoleIdentifier(organization=org, name=name)
130
+ response = await svc.get_role(GetRoleRequest(id=role_id))
131
+
132
+ desired = {Action.Value(_resolve_action_name(a)) for a in actions}
133
+ current = set(response.role.actions)
134
+
135
+ for action in sorted(desired - current):
136
+ await svc.create_role_action(CreateRoleActionRequest(id=role_id, action=action))
137
+ for action in sorted(current - desired):
138
+ await svc.delete_role_action(DeleteRoleActionRequest(id=role_id, action=action))
131
139
 
132
140
  @syncify
133
141
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flyteplugins-union
3
- Version: 0.5.0
3
+ Version: 0.5.2
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/
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.5.2",
3
+ "distance": 0,
4
+ "node": "gea5e734894bf3b85e126c462109c380ee9a427e7",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-13"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.5.0",
3
- "distance": 0,
4
- "node": "gc211548ea59e46226bb178f2cc517031b94c57c4",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-06-29"
8
- }