kaqing 2.0.171__py3-none-any.whl → 2.0.203__py3-none-any.whl

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 (194) hide show
  1. adam/app_session.py +5 -10
  2. adam/apps.py +18 -4
  3. adam/batch.py +7 -7
  4. adam/checks/check_utils.py +3 -1
  5. adam/checks/disk.py +2 -3
  6. adam/columns/memory.py +3 -4
  7. adam/commands/__init__.py +15 -6
  8. adam/commands/alter_tables.py +26 -41
  9. adam/commands/app/__init__.py +0 -0
  10. adam/commands/{app_cmd.py → app/app.py} +2 -2
  11. adam/commands/{show → app}/show_app_actions.py +7 -15
  12. adam/commands/{show → app}/show_app_queues.py +1 -4
  13. adam/{utils_app.py → commands/app/utils_app.py} +9 -1
  14. adam/commands/audit/audit.py +9 -26
  15. adam/commands/audit/audit_repair_tables.py +5 -7
  16. adam/commands/audit/audit_run.py +1 -1
  17. adam/commands/audit/completions_l.py +15 -0
  18. adam/commands/audit/show_last10.py +2 -14
  19. adam/commands/audit/show_slow10.py +2 -13
  20. adam/commands/audit/show_top10.py +2 -11
  21. adam/commands/audit/utils_show_top10.py +15 -3
  22. adam/commands/bash/bash.py +1 -1
  23. adam/commands/bash/utils_bash.py +1 -1
  24. adam/commands/cassandra/__init__.py +0 -0
  25. adam/commands/cassandra/download_cassandra_log.py +45 -0
  26. adam/commands/cassandra/nodetool.py +64 -0
  27. adam/commands/cassandra/nodetool_commands.py +120 -0
  28. adam/commands/cassandra/restart_cluster.py +47 -0
  29. adam/commands/cassandra/restart_node.py +51 -0
  30. adam/commands/cassandra/restart_nodes.py +47 -0
  31. adam/commands/cassandra/rollout.py +88 -0
  32. adam/commands/cat.py +5 -19
  33. adam/commands/cd.py +7 -9
  34. adam/commands/check.py +10 -18
  35. adam/commands/cli_commands.py +6 -1
  36. adam/commands/{cp.py → clipboard_copy.py} +34 -36
  37. adam/commands/code.py +2 -2
  38. adam/commands/command.py +139 -22
  39. adam/commands/commands_utils.py +14 -12
  40. adam/commands/cql/alter_tables.py +66 -0
  41. adam/commands/cql/completions_c.py +29 -0
  42. adam/commands/cql/cqlsh.py +3 -7
  43. adam/commands/cql/utils_cql.py +23 -61
  44. adam/commands/debug/__init__.py +0 -0
  45. adam/commands/debug/debug.py +22 -0
  46. adam/commands/debug/debug_completes.py +35 -0
  47. adam/commands/debug/debug_timings.py +35 -0
  48. adam/commands/deploy/deploy_pg_agent.py +2 -2
  49. adam/commands/deploy/deploy_pod.py +2 -4
  50. adam/commands/deploy/undeploy_pg_agent.py +2 -2
  51. adam/commands/devices/device.py +40 -9
  52. adam/commands/devices/device_app.py +19 -29
  53. adam/commands/devices/device_auit_log.py +3 -3
  54. adam/commands/devices/device_cass.py +17 -23
  55. adam/commands/devices/device_export.py +12 -11
  56. adam/commands/devices/device_postgres.py +79 -63
  57. adam/commands/devices/devices.py +1 -1
  58. adam/commands/download_cassandra_log.py +45 -0
  59. adam/commands/download_file.py +47 -0
  60. adam/commands/export/clean_up_all_export_sessions.py +3 -3
  61. adam/commands/export/clean_up_export_sessions.py +7 -19
  62. adam/commands/export/completions_x.py +11 -0
  63. adam/commands/export/download_export_session.py +40 -0
  64. adam/commands/export/drop_export_database.py +6 -22
  65. adam/commands/export/drop_export_databases.py +3 -9
  66. adam/commands/export/export.py +1 -17
  67. adam/commands/export/export_databases.py +109 -32
  68. adam/commands/export/export_select.py +8 -55
  69. adam/commands/export/export_sessions.py +211 -0
  70. adam/commands/export/export_use.py +13 -16
  71. adam/commands/export/export_x_select.py +48 -0
  72. adam/commands/export/exporter.py +176 -167
  73. adam/commands/export/import_files.py +44 -0
  74. adam/commands/export/import_session.py +10 -6
  75. adam/commands/export/importer.py +24 -9
  76. adam/commands/export/importer_athena.py +114 -44
  77. adam/commands/export/importer_sqlite.py +45 -23
  78. adam/commands/export/show_column_counts.py +11 -20
  79. adam/commands/export/show_export_databases.py +5 -2
  80. adam/commands/export/show_export_session.py +6 -15
  81. adam/commands/export/show_export_sessions.py +4 -11
  82. adam/commands/export/utils_export.py +79 -27
  83. adam/commands/find_files.py +51 -0
  84. adam/commands/find_processes.py +76 -0
  85. adam/commands/generate_report.py +52 -0
  86. adam/commands/head.py +36 -0
  87. adam/commands/help.py +2 -2
  88. adam/commands/intermediate_command.py +6 -3
  89. adam/commands/login.py +3 -6
  90. adam/commands/ls.py +2 -2
  91. adam/commands/medusa/medusa_backup.py +13 -16
  92. adam/commands/medusa/medusa_restore.py +26 -37
  93. adam/commands/medusa/medusa_show_backupjobs.py +7 -7
  94. adam/commands/medusa/medusa_show_restorejobs.py +6 -6
  95. adam/commands/medusa/utils_medusa.py +15 -0
  96. adam/commands/nodetool.py +3 -8
  97. adam/commands/os/__init__.py +0 -0
  98. adam/commands/os/cat.py +36 -0
  99. adam/commands/os/download_file.py +47 -0
  100. adam/commands/os/find_files.py +51 -0
  101. adam/commands/os/find_processes.py +76 -0
  102. adam/commands/os/head.py +36 -0
  103. adam/commands/os/shell.py +41 -0
  104. adam/commands/param_get.py +10 -12
  105. adam/commands/param_set.py +7 -10
  106. adam/commands/postgres/completions_p.py +22 -0
  107. adam/commands/postgres/postgres.py +25 -40
  108. adam/commands/postgres/postgres_databases.py +269 -0
  109. adam/commands/postgres/utils_postgres.py +33 -20
  110. adam/commands/preview_table.py +4 -2
  111. adam/commands/pwd.py +4 -6
  112. adam/commands/reaper/reaper_forward.py +2 -2
  113. adam/commands/reaper/reaper_run_abort.py +4 -10
  114. adam/commands/reaper/reaper_runs.py +3 -3
  115. adam/commands/reaper/reaper_schedule_activate.py +12 -12
  116. adam/commands/reaper/reaper_schedule_start.py +7 -12
  117. adam/commands/reaper/reaper_schedule_stop.py +7 -12
  118. adam/commands/reaper/utils_reaper.py +13 -6
  119. adam/commands/repair/repair_log.py +1 -4
  120. adam/commands/repair/repair_run.py +3 -8
  121. adam/commands/repair/repair_scan.py +1 -6
  122. adam/commands/repair/repair_stop.py +1 -5
  123. adam/commands/restart_cluster.py +47 -0
  124. adam/commands/restart_node.py +51 -0
  125. adam/commands/restart_nodes.py +47 -0
  126. adam/commands/shell.py +9 -2
  127. adam/commands/show/show.py +4 -4
  128. adam/commands/show/show_adam.py +3 -3
  129. adam/commands/show/show_cassandra_repairs.py +5 -6
  130. adam/commands/show/show_cassandra_status.py +29 -29
  131. adam/commands/show/show_cassandra_version.py +1 -4
  132. adam/commands/show/{show_commands.py → show_cli_commands.py} +3 -6
  133. adam/commands/show/show_login.py +3 -9
  134. adam/commands/show/show_params.py +2 -5
  135. adam/commands/show/show_processes.py +15 -16
  136. adam/commands/show/show_storage.py +9 -8
  137. adam/config.py +4 -5
  138. adam/embedded_params.py +1 -1
  139. adam/log.py +4 -4
  140. adam/repl.py +26 -18
  141. adam/repl_commands.py +32 -20
  142. adam/repl_session.py +9 -1
  143. adam/repl_state.py +39 -10
  144. adam/sql/async_executor.py +44 -0
  145. adam/sql/lark_completer.py +286 -0
  146. adam/sql/lark_parser.py +604 -0
  147. adam/sql/qingl.lark +1076 -0
  148. adam/sql/sql_completer.py +4 -6
  149. adam/sql/sql_state_machine.py +25 -13
  150. adam/sso/authn_ad.py +2 -5
  151. adam/sso/authn_okta.py +2 -4
  152. adam/sso/cred_cache.py +2 -5
  153. adam/sso/idp.py +8 -11
  154. adam/utils.py +299 -105
  155. adam/utils_athena.py +18 -18
  156. adam/utils_audits.py +3 -7
  157. adam/utils_issues.py +2 -2
  158. adam/utils_k8s/app_clusters.py +4 -4
  159. adam/utils_k8s/app_pods.py +8 -6
  160. adam/utils_k8s/cassandra_clusters.py +16 -5
  161. adam/utils_k8s/cassandra_nodes.py +9 -6
  162. adam/utils_k8s/custom_resources.py +11 -17
  163. adam/utils_k8s/jobs.py +7 -11
  164. adam/utils_k8s/k8s.py +14 -5
  165. adam/utils_k8s/kube_context.py +3 -6
  166. adam/{pod_exec_result.py → utils_k8s/pod_exec_result.py} +4 -4
  167. adam/utils_k8s/pods.py +85 -23
  168. adam/utils_k8s/statefulsets.py +5 -2
  169. adam/utils_local.py +42 -0
  170. adam/utils_repl/appendable_completer.py +6 -0
  171. adam/utils_repl/repl_completer.py +45 -2
  172. adam/utils_repl/state_machine.py +3 -3
  173. adam/utils_sqlite.py +58 -30
  174. adam/version.py +1 -1
  175. {kaqing-2.0.171.dist-info → kaqing-2.0.203.dist-info}/METADATA +1 -1
  176. kaqing-2.0.203.dist-info/RECORD +277 -0
  177. kaqing-2.0.203.dist-info/top_level.txt +2 -0
  178. teddy/__init__.py +0 -0
  179. teddy/lark_parser.py +436 -0
  180. teddy/lark_parser2.py +618 -0
  181. adam/commands/cql/cql_completions.py +0 -33
  182. adam/commands/export/export_handlers.py +0 -71
  183. adam/commands/export/export_select_x.py +0 -54
  184. adam/commands/logs.py +0 -37
  185. adam/commands/postgres/postgres_context.py +0 -274
  186. adam/commands/postgres/psql_completions.py +0 -10
  187. adam/commands/report.py +0 -61
  188. adam/commands/restart.py +0 -60
  189. kaqing-2.0.171.dist-info/RECORD +0 -236
  190. kaqing-2.0.171.dist-info/top_level.txt +0 -1
  191. /adam/commands/{app_ping.py → app/app_ping.py} +0 -0
  192. /adam/commands/{show → app}/show_app_id.py +0 -0
  193. {kaqing-2.0.171.dist-info → kaqing-2.0.203.dist-info}/WHEEL +0 -0
  194. {kaqing-2.0.171.dist-info → kaqing-2.0.203.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,51 @@
1
+ import os
2
+
3
+ from adam.commands.command import Command
4
+ from adam.repl_state import ReplState
5
+ from adam.utils import log2
6
+ from adam.utils_local import local_tmp_dir
7
+
8
+ class FindLocalFiles(Command):
9
+ COMMAND = 'find local'
10
+
11
+ # the singleton pattern
12
+ def __new__(cls, *args, **kwargs):
13
+ if not hasattr(cls, 'instance'): cls.instance = super(FindLocalFiles, cls).__new__(cls)
14
+
15
+ return cls.instance
16
+
17
+ def __init__(self, successor: Command=None):
18
+ super().__init__(successor)
19
+
20
+ def command(self):
21
+ return FindLocalFiles.COMMAND
22
+
23
+ def run(self, cmd: str, state: ReplState):
24
+ if not(args := self.args(cmd)):
25
+ return super().run(cmd, state)
26
+
27
+ with self.validate(args, state) as (args, state):
28
+ cmd = 'find'
29
+
30
+ if not args:
31
+ cmd = f'find {local_tmp_dir()}'
32
+ elif len(args) == 1:
33
+ cmd = f"find {local_tmp_dir()} -name '{args[0]}'"
34
+ else:
35
+ new_args = [f"'{arg}'" if '*' in arg else arg for arg in args]
36
+ cmd = 'find ' + ' '.join(new_args)
37
+
38
+ log2(cmd)
39
+ os.system(cmd)
40
+
41
+ return state
42
+
43
+ def completion(self, state: ReplState):
44
+ return super().completion(state, {
45
+ '*.csv': None,
46
+ '*.db': None,
47
+ '*': None
48
+ })
49
+
50
+ def help(self, _: ReplState):
51
+ return f'{FindLocalFiles.COMMAND} [linux-find-arguments]\t find files from local machine'
@@ -0,0 +1,76 @@
1
+ from adam.commands import extract_options, validate_args
2
+ from adam.commands.command import Command
3
+ from adam.commands.devices.devices import Devices
4
+ from adam.commands.export.utils_export import state_with_pod
5
+ from adam.repl_state import ReplState, RequiredState
6
+ from adam.utils import log2, tabulize
7
+
8
+ class FindProcesses(Command):
9
+ COMMAND = 'find processes'
10
+
11
+ # the singleton pattern
12
+ def __new__(cls, *args, **kwargs):
13
+ if not hasattr(cls, 'instance'): cls.instance = super(FindProcesses, cls).__new__(cls)
14
+
15
+ return cls.instance
16
+
17
+ def __init__(self, successor: Command=None):
18
+ super().__init__(successor)
19
+
20
+ def command(self):
21
+ return FindProcesses.COMMAND
22
+
23
+ def required(self):
24
+ return [RequiredState.CLUSTER_OR_POD, RequiredState.APP_APP, ReplState.P]
25
+
26
+ def run(self, cmd: str, state: ReplState):
27
+ if not(args := self.args(cmd)):
28
+ return super().run(cmd, state)
29
+
30
+ with self.validate(args, state) as (args, state):
31
+ with extract_options(args, '-kill') as (args, kill):
32
+ with validate_args(args, state, name='words to look for'):
33
+ arg = ' | '.join([f'grep {a}' for a in args])
34
+ awk = "awk '{ print $1, $2, $8, $NF }'"
35
+ rs = Devices.of(state).bash(state, state, f"ps -ef | grep -v grep | {arg} | {awk}".split(' '))
36
+
37
+ lines: list[list[str]] = []
38
+ for r in rs:
39
+ for l in r.stdout.split('\n'):
40
+ l = l.strip(' \t\r\n')
41
+ if not l:
42
+ continue
43
+
44
+ tokens = [r.pod] + l.split(' ')
45
+ lines.append(tokens)
46
+
47
+ pids = []
48
+ for l in lines:
49
+ pids.append(f'{l[2]}@{l[0]}')
50
+
51
+ tabulize(lines, lambda l: '\t'.join(l), header = 'POD\tUSER\tPID\tCMD\tLAST_ARG', separator='\t')
52
+ log2()
53
+ log2(f'PIDS with {",".join(args)}: {",".join(pids)}')
54
+
55
+ if kill:
56
+ log2()
57
+ for pidp in pids:
58
+ pid_n_pod = pidp.split('@')
59
+ pid = pid_n_pod[0]
60
+ if len(pid_n_pod) < 2:
61
+ continue
62
+
63
+ pod = pid_n_pod[1]
64
+
65
+ log2(f'@{pod} bash kill -9 {pid}')
66
+
67
+ with state_with_pod(state, pod) as state1:
68
+ Devices.of(state).bash(state, state1, ['kill', '-9', pid])
69
+
70
+ return rs
71
+
72
+ def completion(self, state: ReplState):
73
+ return super().completion(state)
74
+
75
+ def help(self, _: ReplState):
76
+ return f'{FindProcesses.COMMAND} word... [-kill]\t find processes with words'
@@ -0,0 +1,36 @@
1
+ from adam.commands import validate_args
2
+ from adam.commands.command import Command
3
+ from adam.commands.devices.devices import Devices
4
+ from adam.repl_state import ReplState, RequiredState
5
+
6
+ class Head(Command):
7
+ COMMAND = 'head'
8
+
9
+ # the singleton pattern
10
+ def __new__(cls, *args, **kwargs):
11
+ if not hasattr(cls, 'instance'): cls.instance = super(Head, cls).__new__(cls)
12
+
13
+ return cls.instance
14
+
15
+ def __init__(self, successor: Command=None):
16
+ super().__init__(successor)
17
+
18
+ def command(self):
19
+ return Head.COMMAND
20
+
21
+ def required(self):
22
+ return [RequiredState.CLUSTER_OR_POD, RequiredState.APP_APP, ReplState.P]
23
+
24
+ def run(self, cmd: str, state: ReplState):
25
+ if not(args := self.args(cmd)):
26
+ return super().run(cmd, state)
27
+
28
+ with self.validate(args, state) as (args, state):
29
+ with validate_args(args, state, name='file'):
30
+ return Devices.of(state).bash(state, state, cmd.split(' '))
31
+
32
+ def completion(self, state: ReplState):
33
+ return super().completion(state, lambda: {f: None for f in Devices.of(state).files(state)}, pods=Devices.of(state).pods(state, '-'), auto='jit')
34
+
35
+ def help(self, _: ReplState):
36
+ return f'{Head.COMMAND} file [&]\t run head command on the pod'
@@ -0,0 +1,41 @@
1
+ import os
2
+
3
+ from adam.commands import validate_args
4
+ from adam.commands.command import Command
5
+ from adam.repl_state import ReplState
6
+ from adam.utils import log2
7
+
8
+ class Shell(Command):
9
+ COMMAND = ':sh'
10
+
11
+ # the singleton pattern
12
+ def __new__(cls, *args, **kwargs):
13
+ if not hasattr(cls, 'instance'): cls.instance = super(Shell, cls).__new__(cls)
14
+
15
+ return cls.instance
16
+
17
+ def __init__(self, successor: Command=None):
18
+ super().__init__(successor)
19
+
20
+ def command(self):
21
+ return Shell.COMMAND
22
+
23
+ def run(self, cmd: str, state: ReplState):
24
+ if not(args := self.args(cmd)):
25
+ return super().run(cmd, state)
26
+
27
+ with self.validate(args, state) as (args, _):
28
+ with validate_args(args, state, at_least=0) as args_str:
29
+ if args_str:
30
+ os.system(args_str)
31
+ log2()
32
+ else:
33
+ os.system('QING_DROPPED=true bash')
34
+
35
+ return state
36
+
37
+ def completion(self, state: ReplState):
38
+ return super().completion(state)
39
+
40
+ def help(self, _: ReplState):
41
+ return f'{Shell.COMMAND}\t drop down to shell'
@@ -1,7 +1,8 @@
1
+ from adam.commands import validate_args
1
2
  from adam.commands.command import Command
2
3
  from adam.config import Config
3
4
  from adam.repl_state import ReplState
4
- from adam.utils import lines_to_tabular, log, log2
5
+ from adam.utils import tabulize, log, log2
5
6
 
6
7
  class GetParam(Command):
7
8
  COMMAND = 'get'
@@ -23,19 +24,16 @@ class GetParam(Command):
23
24
  return super().run(cmd, state)
24
25
 
25
26
  with self.validate(args, state) as (args, state):
26
- if len(args) < 1:
27
- lines = [f'{key}\t{Config().get(key, None)}' for key in Config().keys()]
28
- log(lines_to_tabular(lines, separator='\t'))
27
+ def msg():
28
+ tabulize(Config().keys(), lambda key: f'{key}\t{Config().get(key, None)}', separator='\t')
29
29
 
30
- return state
30
+ with validate_args(args, state, msg=msg) as key:
31
+ if v := Config().get(key, None):
32
+ log(v)
33
+ else:
34
+ log2(f'{key} is not set.')
31
35
 
32
- key = args[0]
33
- if v := Config().get(key, None):
34
- log(v)
35
- else:
36
- log2(f'{key} is not set.')
37
-
38
- return v if v else state
36
+ return v if v else state
39
37
 
40
38
  def completion(self, _: ReplState):
41
39
  return {GetParam.COMMAND: {key: None for key in Config().keys()}}
@@ -1,3 +1,4 @@
1
+ from adam.commands import validate_args
1
2
  from adam.commands.command import Command
2
3
  from adam.config import Config
3
4
  from adam.repl_state import ReplState
@@ -23,18 +24,14 @@ class SetParam(Command):
23
24
  return super().run(cmd, state)
24
25
 
25
26
  with self.validate(args, state) as (args, state):
26
- if len(args) < 2:
27
- log2('set <key> <value>')
27
+ with validate_args(args, state, exactly=2, msg=lambda: log2('set <key> <value>')):
28
+ key = args[0]
29
+ value = args[1]
30
+ Config().set(key, value)
28
31
 
29
- return 'invalid args'
32
+ log(Config().get(key, None))
30
33
 
31
- key = args[0]
32
- value = args[1]
33
- Config().set(key, value)
34
-
35
- log(Config().get(key, None))
36
-
37
- return value
34
+ return value
38
35
 
39
36
  def completion(self, _: ReplState):
40
37
  return {SetParam.COMMAND: {key: ({'true': None, 'false': None} if Config().get(key, None) in [True, False] else None) for key in Config().keys()}}
@@ -0,0 +1,22 @@
1
+ from adam.commands.postgres.postgres_databases import PostgresDatabases
2
+ from adam.commands.postgres.utils_postgres import pg_table_names
3
+ from adam.repl_state import ReplState
4
+ from adam.sql.lark_completer import LarkCompleter
5
+
6
+ def completions_p(state: ReplState):
7
+ return {
8
+ '\h': None,
9
+ '\d': None,
10
+ '\dt': None,
11
+ '\du': None
12
+ } | LarkCompleter(expandables={
13
+ 'tables': lambda x: pg_table_names(state),
14
+ 'columns': lambda x: ['id'],
15
+ 'hosts': lambda x: ['@' + PostgresDatabases.pod_and_container(state.namespace)[0]],
16
+ }, variant=ReplState.P).completions_for_nesting()
17
+
18
+ def psql0_completions(state: ReplState):
19
+ return {
20
+ '\h': None,
21
+ '\l': None,
22
+ }
@@ -1,15 +1,15 @@
1
1
  import click
2
2
 
3
- from adam.commands import extract_trailing_options
3
+ from adam.commands import extract_trailing_options, validate_args
4
4
  from adam.commands.command import Command
5
5
  from adam.commands.intermediate_command import IntermediateCommand
6
- from adam.commands.postgres.psql_completions import psql_completions
6
+ from adam.commands.postgres.postgres_databases import pg_path
7
+ from adam.commands.postgres.completions_p import psql0_completions, completions_p
7
8
  from adam.commands.postgres.utils_postgres import pg_table_names, postgres
8
9
  from .postgres_ls import PostgresLs
9
10
  from .postgres_preview import PostgresPreview
10
- from .postgres_context import PostgresContext
11
11
  from adam.repl_state import ReplState
12
- from adam.utils import log, log2
12
+ from adam.utils import log, log2, log_timing
13
13
 
14
14
  class Postgres(IntermediateCommand):
15
15
  COMMAND = 'pg'
@@ -32,55 +32,40 @@ class Postgres(IntermediateCommand):
32
32
 
33
33
  with self.validate(args, state) as (args, state):
34
34
  with extract_trailing_options(args, '&') as (args, backgrounded):
35
- if not args:
36
- if state.in_repl:
37
- log2('Please use SQL statement. e.g. pg \l')
38
- else:
39
- log2('* Command or SQL statements is missing.')
40
- Command.display_help()
35
+ with validate_args(args, state, name='SQL statement') as sql:
36
+ if not state.pg_path:
37
+ if state.in_repl:
38
+ log2('Enter "use <pg-name>" first.')
39
+ else:
40
+ log2('* pg-name is missing.')
41
41
 
42
- return 'command-missing'
42
+ return state
43
43
 
44
- if not state.pg_path:
45
44
  if state.in_repl:
46
- log2('Enter "use <pg-name>" first.')
47
- else:
48
- log2('* pg-name is missing.')
45
+ with postgres(state) as pod:
46
+ pod.sql(args, backgrounded=backgrounded)
47
+ elif not self.run_subcommand(cmd, state):
48
+ with postgres(state) as pod:
49
+ pod.sql(args, backgrounded=backgrounded)
49
50
 
50
51
  return state
51
52
 
52
- if state.in_repl:
53
- with postgres(state) as pod:
54
- pod.sql(args, background=backgrounded)
55
- elif not self.run_subcommand(cmd, state):
56
- with postgres(state) as pod:
57
- pod.sql(args, background=backgrounded)
58
-
59
- return state
60
-
61
53
  def cmd_list(self):
62
54
  return [PostgresLs(), PostgresPreview(), PostgresPg()]
63
55
 
64
56
  def completion(self, state: ReplState):
65
57
  if state.device != state.P:
66
- # conflicts with cql completions
67
58
  return {}
68
59
 
69
- leaf = {}
70
- session = PostgresContext.apply(state.namespace, state.pg_path)
71
- if session.db:
72
- if pg_table_names(state.namespace, state.pg_path):
73
- leaf = psql_completions(state.namespace, state.pg_path)
74
- elif state.pg_path:
75
- leaf = {
76
- '\h': None,
77
- '\l': None,
78
- }
79
-
80
- if state.pg_path:
81
- return super().completion(state, leaf) | leaf
82
- else:
83
- return {}
60
+ with pg_path(state) as (host, database):
61
+ if database:
62
+ if pg_table_names(state):
63
+ with log_timing('psql_completions'):
64
+ return completions_p(state)
65
+ elif host:
66
+ return psql0_completions(state)
67
+
68
+ return {}
84
69
 
85
70
  def help(self, _: ReplState):
86
71
  return f'<sql-statements> [&]\t run queries on Postgres databases'
@@ -0,0 +1,269 @@
1
+ import functools
2
+ import re
3
+ import subprocess
4
+
5
+ from adam.config import Config
6
+ from adam.repl_session import ReplSession
7
+ from adam.repl_state import ReplState
8
+ from adam.utils_k8s.kube_context import KubeContext
9
+ from adam.utils_k8s.pods import Pods
10
+ from adam.utils_k8s.secrets import Secrets
11
+ from adam.utils import log2, log_exc
12
+
13
+ class ConnectionDetails:
14
+ def __init__(self, state: ReplState, namespace: str, host: str):
15
+ self.state = state
16
+ self.namespace = namespace
17
+ self.host = host
18
+
19
+ def endpoint(self):
20
+ return PostgresDatabases._connection_property(self.state, 'pg.secret.endpoint-key', 'postgres-db-endpoint', host=self.host)
21
+
22
+ def port(self):
23
+ return PostgresDatabases._connection_property(self.state, 'pg.secret.port-key', 'postgres-db-port', host=self.host)
24
+
25
+ def username(self):
26
+ return PostgresDatabases._connection_property(self.state, 'pg.secret.username-key', 'postgres-admin-username', host=self.host)
27
+
28
+ def password(self):
29
+ return PostgresDatabases._connection_property(self.state, 'pg.secret.password-key', 'postgres-admin-password', host=self.host)
30
+
31
+ class PostgresDatabases:
32
+ def hosts(state: ReplState, namespace: str = None):
33
+ if not namespace:
34
+ namespace = state.namespace
35
+
36
+ return [ConnectionDetails(state, namespace, host) for host in PostgresDatabases.host_names(namespace)]
37
+
38
+ @functools.lru_cache()
39
+ def host_names(namespace: str):
40
+ ss = Secrets.list_secrets(namespace, name_pattern=Config().get('pg.name-pattern', '^{namespace}.*k8spg.*'))
41
+
42
+ def excludes(name: str):
43
+ exs = Config().get('pg.excludes', '.helm., -admin-secret')
44
+ if exs:
45
+ for ex in exs.split(','):
46
+ if ex.strip(' ') in name:
47
+ return True
48
+
49
+ return False
50
+
51
+ return [s for s in ss if not excludes(s)]
52
+
53
+ def databases(state: ReplState, default_owner = False):
54
+ dbs = []
55
+ # List of databases
56
+ # Name | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access privileges
57
+ # ---------------------------------------+----------+----------+-------------+-------------+------------+-----------------+-----------------------
58
+ # postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | libc |
59
+ # stgawsscpsr_c3_c3 | postgres | UTF8 | C | C | | libc |
60
+ # template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | | libc | =c/postgres +
61
+ # | | | | | | | postgres=CTc/postgres
62
+ # (48 rows)
63
+ if r := PostgresDatabases.run_sql(state, '\l', show_out=False):
64
+ s = 0
65
+ for line in r.stdout.split('\n'):
66
+ line: str = line.strip(' \r')
67
+ if s == 0:
68
+ if 'List of databases' in line:
69
+ s = 1
70
+ elif s == 1:
71
+ if 'Name' in line and 'Owner' in line and 'Encoding' in line:
72
+ s = 2
73
+ elif s == 2:
74
+ if line.startswith('---------'):
75
+ s = 3
76
+ elif s == 3:
77
+ groups = re.match(r'^\s*(\S*)\s*\|\s*(\S*)\s*\|.*', line)
78
+ if groups and groups[1] != '|':
79
+ dbs.append({'name': groups[1], 'owner': groups[2]})
80
+
81
+ if default_owner:
82
+ dbs = [db for db in dbs if db['owner'] == PostgresDatabases.default_owner()]
83
+
84
+ return dbs
85
+
86
+ def tables(state: ReplState, default_schema = False):
87
+ dbs = []
88
+ # List of relations
89
+ # Schema | Name | Type | Owner
90
+ # ----------+------------------------------------------------------------+-------+---------------
91
+ # postgres | c3_2_admin_aclpriv | table | postgres
92
+ # postgres | c3_2_admin_aclpriv_a | table | postgres
93
+ if r := PostgresDatabases.run_sql(state, '\dt', show_out=False):
94
+ s = 0
95
+ for line in r.stdout.split('\n'):
96
+ line: str = line.strip(' \r')
97
+ if s == 0:
98
+ if 'List of relations' in line:
99
+ s = 1
100
+ elif s == 1:
101
+ if 'Schema' in line and 'Name' in line and 'Type' in line:
102
+ s = 2
103
+ elif s == 2:
104
+ if line.startswith('---------'):
105
+ s = 3
106
+ elif s == 3:
107
+ groups = re.match(r'^\s*(\S*)\s*\|\s*(\S*)\s*\|.*', line)
108
+ if groups and groups[1] != '|':
109
+ dbs.append({'schema': groups[1], 'name': groups[2]})
110
+
111
+ if default_schema:
112
+ dbs = [db for db in dbs if db["schema"] == PostgresDatabases.default_schema()]
113
+
114
+ return dbs
115
+
116
+ def run_sql(state: ReplState, sql: str, database: str = None, show_out = True, backgrounded = False):
117
+ if not database:
118
+ database = PostgresDatabases.database(state)
119
+ if not database:
120
+ database = PostgresDatabases.default_db()
121
+
122
+ username = PostgresDatabases.username(state)
123
+ password = PostgresDatabases.password(state)
124
+ endpoint = PostgresDatabases.endpoint(state)
125
+
126
+ if KubeContext.in_cluster():
127
+ cmd1 = f'env PGPASSWORD={password} psql -h {endpoint} -p {PostgresDatabases.port()} -U {username} {database} --pset pager=off -c'
128
+ log2(f'{cmd1} "{sql}"')
129
+ # remove double quotes from the sql argument
130
+ cmd = cmd1.split(' ') + [sql]
131
+
132
+ r = subprocess.run(cmd, capture_output=not backgrounded, text=True)
133
+ if show_out:
134
+ log2(r.stdout)
135
+ log2(r.stderr)
136
+
137
+ return r
138
+ else:
139
+ pod_name, container_name = PostgresDatabases.pod_and_container(state.namespace)
140
+ if not pod_name:
141
+ return
142
+
143
+ cmd = f'psql -h {endpoint} -p {PostgresDatabases.port(state)} -U {username} {database} --pset pager=off -c "{sql}"'
144
+ env_prefix = f'PGPASSWORD="{password}"'
145
+
146
+ r = Pods.exec(pod_name, container_name, state.namespace, cmd, show_out=show_out, backgrounded=backgrounded, env_prefix=env_prefix)
147
+ if r and r.log_file:
148
+ ReplSession().append_history(f'@{r.pod} cat {r.log_file}')
149
+
150
+ return r
151
+
152
+ @functools.lru_cache()
153
+ def pod_and_container(namespace: str):
154
+ container_name = Config().get('pg.agent.name', 'ops-pg-agent')
155
+
156
+ if Config().get('pg.agent.just-in-time', False):
157
+ if not PostgresDatabases.deploy_pg_agent(container_name, namespace):
158
+ return None
159
+
160
+ pod_name = container_name
161
+ try:
162
+ # try with dedicated pg agent pod name configured
163
+ Pods.get(namespace, container_name)
164
+ except:
165
+ try:
166
+ # try with the ops pod
167
+ container_name = Config().get('pod.name', 'ops')
168
+ pod_name = Pods.get_with_selector(namespace, label_selector = Config().get('pod.label-selector', 'run=ops')).metadata.name
169
+ except:
170
+ log2(f"Could not locate {container_name} pod.")
171
+ return None
172
+
173
+ return pod_name, container_name
174
+
175
+ def deploy_pg_agent(pod_name: str, namespace: str) -> str:
176
+ image = Config().get('pg.agent.image', 'seanahnsf/kaqing')
177
+ timeout = Config().get('pg.agent.timeout', 3600)
178
+ try:
179
+ Pods.create(namespace, pod_name, image, ['sleep', f'{timeout}'], env={'NAMESPACE': namespace}, sa_name='c3')
180
+ except Exception as e:
181
+ if e.status == 409:
182
+ if Pods.completed(namespace, pod_name):
183
+ with log_exc(lambda e2: "Exception when calling BatchV1Api->create_pod: %s\n" % e2):
184
+ Pods.delete(pod_name, namespace)
185
+ Pods.create(namespace, pod_name, image, ['sleep', f'{timeout}'], env={'NAMESPACE': namespace}, sa_name='c3')
186
+
187
+ return
188
+ else:
189
+ log2("Exception when calling BatchV1Api->create_pod: %s\n" % e)
190
+
191
+ return
192
+
193
+ Pods.wait_for_running(namespace, pod_name)
194
+
195
+ return pod_name
196
+
197
+ def undeploy_pg_agent(pod_name: str, namespace: str):
198
+ Pods.delete(pod_name, namespace, grace_period_seconds=0)
199
+
200
+ def endpoint(state: ReplState):
201
+ return PostgresDatabases._connection_property(state, 'pg.secret.endpoint-key', 'postgres-db-endpoint')
202
+
203
+ def port(state: ReplState):
204
+ return PostgresDatabases._connection_property(state, 'pg.secret.port-key', 'postgres-db-port')
205
+
206
+ def username(state: ReplState):
207
+ return PostgresDatabases._connection_property(state, 'pg.secret.username-key', 'postgres-admin-username')
208
+
209
+ def password(state: ReplState):
210
+ return PostgresDatabases._connection_property(state, 'pg.secret.password-key', 'postgres-admin-password')
211
+
212
+ def _connection_property(state: ReplState, config_key: str, default: str, host: str = None, database: str = None):
213
+ with pg_path(state, host=host, database=database) as (host, _):
214
+ if not (conn := PostgresDatabases.conn_details(state.namespace, host)):
215
+ return ''
216
+
217
+ key = Config().get(config_key, default)
218
+ return conn[key] if key in conn else ''
219
+
220
+ def default_db():
221
+ return Config().get('pg.default-db', 'postgres')
222
+
223
+ def default_owner():
224
+ return Config().get('pg.default-owner', 'postgres')
225
+
226
+ def default_schema():
227
+ return Config().get('pg.default-schema', 'postgres')
228
+
229
+ def host(state: ReplState):
230
+ if not state.pg_path:
231
+ return None
232
+
233
+ return state.pg_path.split('/')[0]
234
+
235
+ def database(state: ReplState):
236
+ if not state.pg_path:
237
+ return None
238
+
239
+ tokens = state.pg_path.split('/')
240
+ if len(tokens) > 1:
241
+ return tokens[1]
242
+
243
+ return None
244
+
245
+ @functools.lru_cache()
246
+ def conn_details(namespace: str, host: str):
247
+ return Secrets.get_data(namespace, host)
248
+
249
+ class PostgresPathHandler:
250
+ def __init__(self, state: ReplState, host: str = None, database: str = None):
251
+ self.state = state
252
+ self.host = host
253
+ self.database = database
254
+
255
+ def __enter__(self) -> tuple[str, str]:
256
+ if self.state and self.state.pg_path:
257
+ host_n_db = self.state.pg_path.split('/')
258
+ if not self.host:
259
+ self.host = host_n_db[0]
260
+ if not self.database and len(host_n_db) > 1:
261
+ self.database = host_n_db[1]
262
+
263
+ return self.host, self.database
264
+
265
+ def __exit__(self, exc_type, exc_val, exc_tb):
266
+ return False
267
+
268
+ def pg_path(state: ReplState, host: str = None, database: str = None):
269
+ return PostgresPathHandler(state, host=host, database=database)