Skip to content

Commit 4ce824f

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents fb33e33 + 7e07f19 commit 4ce824f

File tree

8 files changed

+319
-29
lines changed

8 files changed

+319
-29
lines changed

doc/changes/unreleased.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
## Features
44

5-
* #66: Implement a standard CLI options builder.
6-
* #69: Add create_bucketfs_location function.
5+
* #66: Implemented a standard CLI options builder.
6+
* #70: Implemented a CLI callback function for the language container deployment.
7+
* #69: Added create_bucketfs_location function.
78

89
# Refactoring
910

10-
* #68: Make open_pyexasol_connection(...) using kwargs derived from the cli options.
11+
* #68: Made open_pyexasol_connection(...) using kwargs derived from the cli options.

doc/user_guide/user-guide.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ similar to the one below.
1515
python -m <exasol_extension>.deploy language-container <options>
1616
```
1717

18-
The name of the script (```<exasol_extension>.deploy``` in the above command) can vary from one extension to another.
19-
Please check the user guide of a particular extension. The rest of the command line will have a common format. It
20-
will include the command - ```language-container``` - and selected options. The choice of options is primarily
21-
determined by the storage backend being used - On-Prem or SaaS.
18+
The name of the script (```<exasol_extension>.deploy``` in the above command) and the command name
19+
(e.g. ```language-container```) can vary from one extension to another. Please check the user guide of a particular
20+
extension. The rest of the command line will have a common format. It will include some of the options defined below.
21+
The choice of options is primarily determined by the storage backend being used - On-Prem or SaaS.
2222

2323
### List of options
2424

@@ -55,13 +55,13 @@ another source.
5555
| version | [x] | [x] | Optional, provide for downloading SLC from GitHub |
5656
| container-file | [x] | [x] | Optional, provide for uploading SLC file |
5757
| ssl-cert-path | [x] | [x] | Optional |
58-
| [no_]use-ssl-cert-validation | [x] | [x] | Optional boolean, defaults to True |
58+
| [no-]use-ssl-cert-validation | [x] | [x] | Optional boolean, defaults to True |
5959
| ssl-client-cert-path | [x] | | Optional |
6060
| ssl-client-private-key | [x] | | Optional |
61-
| [no_]upload-container | [x] | [x] | Optional boolean, defaults to True |
62-
| [no_]alter-system | [x] | [x] | Optional boolean, defaults to True |
63-
| [dis]allow-override | [x] | [x] | Optional boolean, defaults to False |
64-
| [no_]wait_for_completion | [x] | [x] | Optional boolean, defaults to True |
61+
| [no-]upload-container | [x] | [x] | Optional boolean, defaults to True |
62+
| [no-]alter-system | [x] | [x] | Optional boolean, defaults to True |
63+
| [no-]allow-override | [x] | [x] | Optional boolean, defaults to False |
64+
| [no-]wait_for_completion | [x] | [x] | Optional boolean, defaults to True |
6565

6666
### Container selection
6767

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from pathlib import Path
2+
3+
from exasol.python_extension_common.deployment.language_container_deployer import LanguageContainerDeployer
4+
from exasol.python_extension_common.connections.pyexasol_connection import open_pyexasol_connection
5+
from exasol.python_extension_common.connections.bucketfs_location import create_bucketfs_location
6+
from exasol.python_extension_common.cli.std_options import StdParams
7+
8+
9+
class LanguageContainerDeployerCli:
10+
"""
11+
The class provides a CLI callback function that creates and runs the
12+
LangaugeContainerDeployer.
13+
14+
At first glance, it may look a bit over-designed. The reason for wrapping a
15+
callback function in a class is to let the user define the option names for the
16+
container URL and container name. These options are not defined in the StdParams
17+
but rather generated by formatters. The user can give them arbitrary names.
18+
Hence, we don't want to assume any particular names in the callback function.
19+
"""
20+
21+
def __init__(self,
22+
container_url_arg: str | None = None,
23+
container_name_arg: str | None = None) -> None:
24+
self._container_url_arg = container_url_arg
25+
self._container_name_arg = container_name_arg
26+
27+
def __call__(self, **kwargs):
28+
29+
pyexasol_connection = open_pyexasol_connection(**kwargs)
30+
bucketfs_location = create_bucketfs_location(**kwargs)
31+
32+
language_alias = kwargs[StdParams.language_alias.name]
33+
container_file = kwargs[StdParams.container_file.name]
34+
upload_container = kwargs[StdParams.upload_container.name]
35+
alter_system = kwargs[StdParams.alter_system.name]
36+
allow_override = kwargs[StdParams.allow_override.name]
37+
wait_for_completion = kwargs[StdParams.wait_for_completion.name]
38+
39+
deployer = LanguageContainerDeployer(pyexasol_connection,
40+
language_alias,
41+
bucketfs_location)
42+
if not upload_container:
43+
deployer.run(alter_system=alter_system,
44+
allow_override=allow_override,
45+
wait_for_completion=wait_for_completion)
46+
elif container_file:
47+
deployer.run(container_file=Path(container_file),
48+
alter_system=alter_system,
49+
allow_override=allow_override,
50+
wait_for_completion=wait_for_completion)
51+
elif kwargs.get(self._container_url_arg) and kwargs.get(self._container_name_arg):
52+
deployer.download_and_run(kwargs[self._container_url_arg],
53+
kwargs[self._container_name_arg],
54+
alter_system=alter_system,
55+
allow_override=allow_override,
56+
wait_for_completion=wait_for_completion)
57+
else:
58+
raise ValueError("To upload a language container either its release version "
59+
f"(--{StdParams.version.name}) or a path of the already "
60+
f"downloaded container file (--{StdParams.container_file.name}) "
61+
"must be provided.")

exasol/python_extension_common/deployment/language_container_deployer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import tempfile
88
import ssl
99
import requests # type: ignore
10+
import warnings
1011
import pyexasol # type: ignore
1112
import exasol.bucketfs as bfs # type: ignore
1213
from exasol.saas.client.api_access import (get_connection_params, get_database_id) # type: ignore
@@ -330,7 +331,12 @@ def create(cls,
330331
use_ssl_cert_validation: bool = True, ssl_trusted_ca: Optional[str] = None,
331332
ssl_client_certificate: Optional[str] = None,
332333
ssl_private_key: Optional[str] = None) -> "LanguageContainerDeployer":
333-
334+
warnings.warn(
335+
"create() function is deprecated and will be removed in a future version. "
336+
"For CLI use the LanguageContainerDeployerCli class instead.",
337+
DeprecationWarning,
338+
stacklevel=2
339+
)
334340
# Infer where the database is - on-prem or SaaS.
335341
if all((dsn, db_user, db_password, bucketfs_host, bucketfs_port,
336342
bucketfs_name, bucket, bucketfs_user, bucketfs_password)):

exasol/python_extension_common/deployment/language_container_deployer_cli.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from enum import Enum
55
from pathlib import Path
66
import click
7+
import warnings
78
from exasol.python_extension_common.deployment.language_container_deployer import LanguageContainerDeployer
89

910

@@ -183,6 +184,12 @@ def language_container_deployer_main(
183184
wait_for_completion: bool,
184185
container_url: Optional[str] = None,
185186
container_name: Optional[str] = None):
187+
warnings.warn(
188+
"language_container_deployer_main() function is deprecated and will be removed "
189+
"in a future version. For CLI use the LanguageContainerDeployerCli class instead.",
190+
DeprecationWarning,
191+
stacklevel=2
192+
)
186193

187194
deployer = LanguageContainerDeployer.create(
188195
bucketfs_name=bucketfs_name,
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
from typing import Any
2+
from contextlib import ExitStack
3+
from urllib.parse import urlparse
4+
import pytest
5+
import click
6+
from click.testing import CliRunner
7+
8+
from exasol.python_extension_common.cli.std_options import (
9+
StdTags, StdParams, ParameterFormatters, select_std_options)
10+
from exasol.python_extension_common.connections.pyexasol_connection import (
11+
open_pyexasol_connection)
12+
from exasol.python_extension_common.cli.language_container_deployer_cli import (
13+
LanguageContainerDeployerCli)
14+
from test.utils.db_utils import (assert_udf_running, create_schema)
15+
from test.utils.revert_language_settings import revert_language_settings
16+
17+
CONTAINER_URL_ARG = 'container_url'
18+
CONTAINER_NAME_ARG = 'container_name'
19+
20+
21+
@pytest.fixture(scope='session')
22+
def onprem_cli_args(backend_aware_onprem_database,
23+
exasol_config,
24+
bucketfs_config,
25+
language_alias) -> dict[str, Any]:
26+
27+
parsed_url = urlparse(bucketfs_config.url)
28+
host, port = parsed_url.netloc.split(":")
29+
return {
30+
StdParams.dsn.name: f'{exasol_config.host}:{exasol_config.port}',
31+
StdParams.db_user.name: exasol_config.username,
32+
StdParams.db_password.name: exasol_config.password,
33+
StdParams.bucketfs_host.name: host,
34+
StdParams.bucketfs_port.name: port,
35+
StdParams.bucketfs_use_https.name: parsed_url.scheme.lower() == 'https',
36+
StdParams.bucketfs_user.name: bucketfs_config.username,
37+
StdParams.bucketfs_password.name: bucketfs_config.password,
38+
StdParams.bucketfs_name.name: 'bfsdefault',
39+
StdParams.bucket.name: 'default',
40+
StdParams.use_ssl_cert_validation.name: False,
41+
}
42+
43+
44+
@pytest.fixture(scope='session')
45+
def saas_cli_args(saas_host,
46+
saas_pat,
47+
saas_account_id,
48+
backend_aware_saas_database_id,
49+
) -> dict[str, Any]:
50+
return {
51+
StdParams.saas_url.name: saas_host,
52+
StdParams.saas_account_id.name: saas_account_id,
53+
StdParams.saas_database_id.name: backend_aware_saas_database_id,
54+
StdParams.saas_token.name: saas_pat
55+
}
56+
57+
58+
@pytest.fixture(scope='session')
59+
def slc_cli_args(language_alias) -> dict[str, Any]:
60+
return {
61+
StdParams.alter_system.name: True,
62+
StdParams.allow_override.name: True,
63+
StdParams.wait_for_completion.name: True,
64+
StdParams.path_in_bucket.name: 'container',
65+
StdParams.language_alias.name: language_alias
66+
}
67+
68+
69+
def create_deploy_command(backend_tag: StdTags,
70+
container_name: str | None = None,
71+
container_url_formatter: str | None = None) -> click.Command:
72+
"""
73+
This is a blueprint for creating an isolated click Command
74+
for the language container deployment.
75+
76+
backend_tag should be either StdTags.ONPREM or StdTags.SAAS.
77+
"""
78+
if container_name and container_url_formatter:
79+
ver_formatter = ParameterFormatters()
80+
ver_formatter.set_formatter(CONTAINER_URL_ARG, container_url_formatter)
81+
ver_formatter.set_formatter(CONTAINER_NAME_ARG, container_name)
82+
formatters = {StdParams.version: ver_formatter}
83+
else:
84+
formatters = None
85+
86+
opts = select_std_options(
87+
[StdTags.DB | backend_tag, StdTags.BFS | backend_tag, StdTags.SLC],
88+
formatters=formatters)
89+
cli_callback = LanguageContainerDeployerCli(
90+
container_url_arg=CONTAINER_URL_ARG,
91+
container_name_arg=CONTAINER_NAME_ARG)
92+
93+
return click.Command('deploy_slc', params=opts, callback=cli_callback)
94+
95+
96+
def make_args_string(**kwargs) -> str:
97+
def arg_string(k: str, v: Any):
98+
k = k.replace("_", "-")
99+
if isinstance(v, bool):
100+
return f'--{k}' if v else f'--no-{k}'
101+
return f'--{k} "{v}"'
102+
103+
return ' '.join(arg_string(k, v) for k, v in kwargs.items())
104+
105+
106+
def run_deploy_command(deploy_command: click.Command,
107+
arg_string: str,
108+
language_alias: str,
109+
db_schema: str,
110+
db_params: dict[str, Any]):
111+
112+
with ExitStack() as stack:
113+
conn_before = stack.enter_context(open_pyexasol_connection(**db_params))
114+
stack.enter_context(revert_language_settings(conn_before))
115+
116+
runner = CliRunner()
117+
runner.invoke(deploy_command, args=arg_string,
118+
catch_exceptions=False, standalone_mode=False)
119+
120+
# We have to open another connection because the language settings on
121+
# the previously opened connection are unaffected by the slc deployment.
122+
conn_after = stack.enter_context(open_pyexasol_connection(**db_params))
123+
create_schema(conn_after, db_schema)
124+
assert_udf_running(conn_after, language_alias, db_schema)
125+
126+
127+
def test_slc_deployer_cli_onprem_url(use_onprem,
128+
container_version,
129+
container_name,
130+
container_url_formatter,
131+
language_alias,
132+
db_schema,
133+
onprem_cli_args,
134+
slc_cli_args):
135+
if not use_onprem:
136+
pytest.skip("The test is not configured to use ITDE.")
137+
138+
deploy_command = create_deploy_command(StdTags.ONPREM,
139+
container_name=container_name,
140+
container_url_formatter=container_url_formatter)
141+
extra_cli_args = {StdParams.version.name: container_version}
142+
arg_string = make_args_string(**onprem_cli_args, **slc_cli_args, **extra_cli_args)
143+
run_deploy_command(deploy_command, arg_string, language_alias, db_schema, onprem_cli_args)
144+
145+
146+
def test_slc_deployer_cli_onprem_file(use_onprem,
147+
container_path,
148+
language_alias,
149+
db_schema,
150+
onprem_cli_args,
151+
slc_cli_args):
152+
if not use_onprem:
153+
pytest.skip("The test is not configured to use ITDE.")
154+
155+
deploy_command = create_deploy_command(StdTags.ONPREM)
156+
extra_cli_args = {StdParams.container_file.name: container_path}
157+
arg_string = make_args_string(**onprem_cli_args, **slc_cli_args, **extra_cli_args)
158+
run_deploy_command(deploy_command, arg_string, language_alias, db_schema, onprem_cli_args)
159+
160+
161+
def test_slc_deployer_cli_saas_url(use_saas,
162+
container_version,
163+
container_name,
164+
container_url_formatter,
165+
language_alias,
166+
db_schema,
167+
saas_cli_args,
168+
slc_cli_args):
169+
if not use_saas:
170+
pytest.skip("The test is not configured to run in SaaS.")
171+
172+
deploy_command = create_deploy_command(StdTags.SAAS,
173+
container_name=container_name,
174+
container_url_formatter=container_url_formatter)
175+
extra_cli_args = {StdParams.version.name: container_version}
176+
arg_string = make_args_string(**saas_cli_args, **slc_cli_args, **extra_cli_args)
177+
run_deploy_command(deploy_command, arg_string, language_alias, db_schema, saas_cli_args)
178+
179+
180+
def test_slc_deployer_cli_saas_file(use_saas,
181+
container_path,
182+
language_alias,
183+
db_schema,
184+
saas_cli_args,
185+
slc_cli_args):
186+
if not use_saas:
187+
pytest.skip("The test is not configured to run in SaaS.")
188+
189+
deploy_command = create_deploy_command(StdTags.SAAS)
190+
extra_cli_args = {StdParams.container_file.name: container_path}
191+
arg_string = make_args_string(**saas_cli_args, **slc_cli_args, **extra_cli_args)
192+
run_deploy_command(deploy_command, arg_string, language_alias, db_schema, saas_cli_args)

0 commit comments

Comments
 (0)