Skip to content

Commit 975d19b

Browse files
- Works e2e no verification.
1 parent d5f4e5a commit 975d19b

File tree

5 files changed

+166
-31
lines changed

5 files changed

+166
-31
lines changed

cicd/keys/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
*
22
!.gitignore
33
!integration/
4-
!work/
4+
!work/
5+
!testing/

cicd/keys/testing/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
## Setup
3+
4+
First, create a google service account key using the GCP Console, per [the GCP documentation](https://cloud.google.com/iam/docs/keys-create-delete). Grant the service account at least `Viewer` role equivalent privileges, per [the GCP dumentation](https://cloud.google.com/iam/docs/create-service-agents#grant-roles).
5+
6+
Then, do this in bash:
7+
8+
```bash setup stackql-shell
9+
10+
export GOOGLE_CREDENTIALS="$(cat cicd/keys/testing/google-credentials.json)";
11+
12+
stackql shell --approot=./test/tmp/.get-google-vms.stackql
13+
```
14+
15+
## Method
16+
17+
Do this in the `stackql` shell, replacing `<project>` with your GCP project name:
18+
19+
```sql stackql-shell input required project=ryuki-it-sandbox-01
20+
21+
registry pull google;
22+
23+
select
24+
name,
25+
id
26+
FROM google.compute.instances
27+
WHERE
28+
project = 'ryuki-it-sandbox-01'
29+
AND zone = 'australia-southeast1-a'
30+
;
31+
32+
```
33+
34+
## Result
35+
36+
37+
You will see something very much like this included in the output, presuming you have one VM (if you have zero, only the headers should appper, more VMs means more rows):
38+
39+
```sql stackql stdout table-contains-data
40+
|--------------------------------------------------|---------------------|
41+
| name | id |
42+
|--------------------------------------------------|---------------------|
43+
| any-compute-cluster-1-default-abcd-00000001-0001 | 1000000000000000001 |
44+
|--------------------------------------------------|---------------------|
45+
```
46+
47+
<!--- STDERR_REGEX_EXACT
48+
google\ provider,\ version\ 'v24.11.00274'\ successfully\ installed
49+
goodbye
50+
-->
51+
52+
## Cleanup
53+
54+
```bash teardown best-effort
55+
56+
rm -rf ./test/tmp/.get-google-vms.stackql
57+
58+
```

test/python/markdown_testing/markdown_testing.py

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,26 @@
22

33
from typing import List, Tuple
44

5-
import subprocess, os, sys
5+
import subprocess, os, sys, shutil, io
6+
7+
import json
8+
9+
_REPOSITORY_ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..'))
10+
11+
"""
12+
Intentions:
13+
14+
- Support markdown parsing.
15+
- Support sequential markdown code block execution, leveraging [info strings](https://spec.commonmark.org/0.30/#info-string).
16+
"""
617

718
class ASTNode(object):
819

20+
_STACKQL_SHELL_INVOCATION: str = 'stackql-shell'
21+
_BASH: str = 'bash'
22+
_SETUP: str = 'setup'
23+
_TEARDOWN: str = 'teardown'
24+
925
def __init__(self, node: dict):
1026
self.node = node
1127
self.children = []
@@ -17,38 +33,44 @@ def get_type(self) -> str:
1733
return self.node.get('type', '')
1834

1935
def get_text(self) -> str:
20-
return self.node.get('text', '')
36+
return self.node.get('raw', '').strip()
2137

2238
def is_executable(self) -> bool:
23-
return self.get_type() == 'code_block'
39+
return self.get_type() == 'block_code'
40+
41+
def _get_annotations(self) -> List[str]:
42+
return self.node.get('attrs').get('info', '').split(' ')
43+
44+
def is_stackql_shell_invocation(self) -> bool:
45+
return self._STACKQL_SHELL_INVOCATION in self._get_annotations()
46+
47+
def is_bash(self) -> bool:
48+
return self._BASH in self._get_annotations()
49+
50+
def is_setup(self) -> bool:
51+
return self._SETUP in self._get_annotations()
52+
53+
def is_teardown(self) -> bool:
54+
return self._TEARDOWN in self._get_annotations()
2455

2556
def get_execution_language(self) -> str:
2657
return self.node.get('lang', '')
2758

59+
def __str__(self):
60+
return json.dumps(self.node, indent=2)
61+
62+
def __repr__(self):
63+
return self.__str__()
2864

2965
class MdParser(object):
3066

3167
def parse_markdown_file(self, file_path: str, lang=None) -> List[ASTNode]:
3268
markdown: mistune.Markdown = mistune.create_markdown(renderer='ast')
3369
with open(file_path, 'r') as f:
3470
txt = f.read()
35-
return markdown(txt)
36-
37-
38-
class MdExecutor(object):
71+
raw_list: List[dict] = markdown(txt)
72+
return [ASTNode(node) for node in raw_list]
3973

40-
def __init__(self, renderer: MdParser):
41-
self.renderer = renderer
42-
43-
def execute(self, file_path: str) -> None:
44-
ast = self.renderer.parse_markdown_file(file_path)
45-
for node in ast:
46-
if node.is_executable():
47-
lang = node.get_execution_language()
48-
if lang == 'python':
49-
exec(node.get_text())
50-
else:
51-
print(f'Unsupported language: {lang}')
5274

5375
class WorkloadDTO(object):
5476

@@ -65,20 +87,75 @@ def get_in_session(self) -> List[str]:
6587

6688
def get_teardown(self) -> List[str]:
6789
return self._teardown
90+
91+
def __str__(self):
92+
return f'Setup: {self._setup}\nIn Session: {self._in_session}\nTeardown: {self._teardown}'
93+
94+
def __repr__(self):
95+
return self.__str__()
96+
97+
class MdOrchestrator(object):
98+
99+
def __init__(
100+
self,
101+
parser: MdParser,
102+
max_setup_blocks: int = 1,
103+
max_invocations_blocks: int = 1,
104+
max_teardown_blocks: int = 1,
105+
setup_contains_shell_invocation: bool = True
106+
):
107+
self._parser = parser
108+
self._max_setup_blocks = max_setup_blocks
109+
self._max_invocations_blocks = max_invocations_blocks
110+
self._max_teardown_blocks = max_teardown_blocks
111+
self._setup_contains_shell_invocation = setup_contains_shell_invocation
112+
113+
def orchestrate(self, file_path: str) -> WorkloadDTO:
114+
setup_count: int = 0
115+
teardown_count: int = 0
116+
invocation_count: int = 0
117+
ast = self._parser.parse_markdown_file(file_path)
118+
print(f'AST: {ast}')
119+
setup_str: str = f'cd {_REPOSITORY_ROOT_PATH};\n'
120+
in_session_commands: List[str] = []
121+
teardown_str: str = f'cd {_REPOSITORY_ROOT_PATH};\n'
122+
for node in ast:
123+
if node.is_executable():
124+
if node.is_setup():
125+
if setup_count < self._max_setup_blocks:
126+
setup_str += f'{node.get_text()}'
127+
setup_count += 1
128+
else:
129+
raise KeyError(f'Maximum setup blocks exceeded: {self._max_setup_blocks}')
130+
elif node.is_teardown():
131+
if teardown_count < self._max_teardown_blocks:
132+
teardown_str += f'{node.get_text()}'
133+
teardown_count += 1
134+
else:
135+
raise KeyError(f'Maximum teardown blocks exceeded: {self._max_teardown_blocks}')
136+
elif node.is_stackql_shell_invocation():
137+
if invocation_count < self._max_invocations_blocks:
138+
all_commands: str = node.get_text().split('\n\n')
139+
in_session_commands += all_commands
140+
invocation_count += 1
141+
else:
142+
raise KeyError(f'Maximum invocation blocks exceeded: {self._max_invocations_blocks}')
143+
return WorkloadDTO(setup_str, in_session_commands, teardown_str)
68144

69145
class SimpleE2E(object):
70146

71147
def __init__(self, workload: WorkloadDTO):
72148
self._workload = workload
73149

74150
def run(self) -> Tuple[bytes, bytes]:
151+
bash_path = shutil.which('bash')
75152
pr: subprocess.Popen = subprocess.Popen(
76153
self._workload.get_setup(),
77154
stdin=subprocess.PIPE,
78155
stdout=subprocess.PIPE,
79156
stderr=subprocess.PIPE,
80157
shell=True,
81-
executable='/bin/bash'
158+
executable=bash_path
82159
)
83160
for cmd in self._workload.get_in_session():
84161
pr.stdin.write(f"{cmd}\n".encode(sys.getdefaultencoding()))
@@ -87,16 +164,11 @@ def run(self) -> Tuple[bytes, bytes]:
87164
return (stdoout_bytes, stderr_bytes,)
88165

89166
if __name__ == '__main__':
90-
workload_dto: WorkloadDTO = WorkloadDTO(
91-
setup="""
92-
export GOOGLE_CREDENTIALS="$(cat /Users/admin/stackql/secrets/concerted-testing/google-credentials.json)" &&
93-
stackql shell""",
94-
in_session=[
95-
'registry pull google;',
96-
'select name, id FROM google.compute.instances WHERE project = \'ryuki-it-sandbox-01\' AND zone = \'australia-southeast1-a\';'
97-
],
98-
teardown='echo "Goodbye, World!"'
99-
)
167+
md_parser = MdParser()
168+
orchestrator: MdOrchestrator = MdOrchestrator(md_parser)
169+
workload_dto: WorkloadDTO = orchestrator.orchestrate(os.path.join(_REPOSITORY_ROOT_PATH, 'docs', 'walkthroughs', 'get-google-vms.md'))
170+
print(f'Workload DTO: {workload_dto}')
171+
# print(json.dumps(parsed_file, indent=2))
100172
e2e: SimpleE2E = SimpleE2E(workload_dto)
101173
stdout_bytes, stderr_bytes = e2e.run()
102174
print(stdout_bytes.decode(sys.getdefaultencoding()))

test/tmp/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

0 commit comments

Comments
 (0)