snowflake-cli-labs 3.0.0rc1__py3-none-any.whl → 3.0.0rc2__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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +10 -1
- snowflake/cli/_app/snow_connector.py +76 -29
- snowflake/cli/_app/telemetry.py +8 -4
- snowflake/cli/_app/version_check.py +74 -0
- snowflake/cli/_plugins/git/commands.py +55 -14
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -5
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +49 -31
- snowflake/cli/_plugins/nativeapp/manager.py +46 -87
- snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +9 -152
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
- snowflake/cli/_plugins/snowpark/commands.py +1 -1
- snowflake/cli/_plugins/snowpark/models.py +2 -1
- snowflake/cli/_plugins/streamlit/commands.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +9 -0
- snowflake/cli/_plugins/workspace/action_context.py +2 -1
- snowflake/cli/_plugins/workspace/commands.py +48 -16
- snowflake/cli/_plugins/workspace/manager.py +1 -0
- snowflake/cli/api/cli_global_context.py +136 -313
- snowflake/cli/api/commands/flags.py +76 -91
- snowflake/cli/api/commands/snow_typer.py +6 -4
- snowflake/cli/api/config.py +1 -1
- snowflake/cli/api/connections.py +214 -0
- snowflake/cli/api/console/abc.py +4 -2
- snowflake/cli/api/entities/application_entity.py +687 -2
- snowflake/cli/api/entities/application_package_entity.py +151 -46
- snowflake/cli/api/entities/common.py +1 -0
- snowflake/cli/api/entities/utils.py +41 -17
- snowflake/cli/api/identifiers.py +3 -0
- snowflake/cli/api/project/definition.py +11 -0
- snowflake/cli/api/project/definition_conversion.py +171 -13
- snowflake/cli/api/project/schemas/entities/common.py +0 -12
- snowflake/cli/api/project/schemas/identifier_model.py +2 -2
- snowflake/cli/api/project/schemas/project_definition.py +101 -39
- snowflake/cli/api/rendering/project_definition_templates.py +4 -0
- snowflake/cli/api/rendering/sql_templates.py +7 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -1
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +6 -6
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +44 -42
- snowflake/cli/api/commands/typer_pre_execute.py +0 -26
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,86 @@
|
|
|
1
|
-
from
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
from typing import Callable, List, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from click import ClickException, UsageError
|
|
8
|
+
from snowflake.cli._plugins.nativeapp.common_flags import (
|
|
9
|
+
ForceOption,
|
|
10
|
+
InteractiveOption,
|
|
11
|
+
ValidateOption,
|
|
12
|
+
)
|
|
13
|
+
from snowflake.cli._plugins.nativeapp.constants import (
|
|
14
|
+
ALLOWED_SPECIAL_COMMENTS,
|
|
15
|
+
COMMENT_COL,
|
|
16
|
+
NAME_COL,
|
|
17
|
+
OWNER_COL,
|
|
18
|
+
PATCH_COL,
|
|
19
|
+
SPECIAL_COMMENT,
|
|
20
|
+
VERSION_COL,
|
|
21
|
+
)
|
|
22
|
+
from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
23
|
+
ApplicationPackageDoesNotExistError,
|
|
24
|
+
)
|
|
25
|
+
from snowflake.cli._plugins.nativeapp.policy import (
|
|
26
|
+
AllowAlwaysPolicy,
|
|
27
|
+
AskAlwaysPolicy,
|
|
28
|
+
DenyAlwaysPolicy,
|
|
29
|
+
PolicyBase,
|
|
30
|
+
)
|
|
31
|
+
from snowflake.cli._plugins.nativeapp.same_account_install_method import (
|
|
32
|
+
SameAccountInstallMethod,
|
|
33
|
+
)
|
|
34
|
+
from snowflake.cli._plugins.nativeapp.utils import (
|
|
35
|
+
needs_confirmation,
|
|
36
|
+
)
|
|
37
|
+
from snowflake.cli._plugins.workspace.action_context import ActionContext
|
|
38
|
+
from snowflake.cli.api.console.abc import AbstractConsole
|
|
39
|
+
from snowflake.cli.api.entities.application_package_entity import (
|
|
40
|
+
ApplicationPackageEntity,
|
|
41
|
+
)
|
|
42
|
+
from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
|
|
43
|
+
from snowflake.cli.api.entities.utils import (
|
|
44
|
+
drop_generic_object,
|
|
45
|
+
ensure_correct_owner,
|
|
46
|
+
execute_post_deploy_hooks,
|
|
47
|
+
generic_sql_error_handler,
|
|
48
|
+
print_messages,
|
|
49
|
+
)
|
|
50
|
+
from snowflake.cli.api.errno import (
|
|
51
|
+
APPLICATION_NO_LONGER_AVAILABLE,
|
|
52
|
+
CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION,
|
|
53
|
+
CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES,
|
|
54
|
+
NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
|
|
55
|
+
ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
|
|
56
|
+
)
|
|
57
|
+
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
|
|
2
58
|
from snowflake.cli.api.project.schemas.entities.application_entity_model import (
|
|
3
59
|
ApplicationEntityModel,
|
|
4
60
|
)
|
|
61
|
+
from snowflake.cli.api.project.schemas.entities.application_package_entity_model import (
|
|
62
|
+
ApplicationPackageEntityModel,
|
|
63
|
+
)
|
|
64
|
+
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
|
|
65
|
+
from snowflake.cli.api.project.util import (
|
|
66
|
+
extract_schema,
|
|
67
|
+
identifier_to_show_like_pattern,
|
|
68
|
+
unquote_identifier,
|
|
69
|
+
)
|
|
70
|
+
from snowflake.cli.api.utils.cursor import find_all_rows
|
|
71
|
+
from snowflake.connector import ProgrammingError
|
|
72
|
+
from snowflake.connector.cursor import DictCursor
|
|
73
|
+
|
|
74
|
+
# Reasons why an `alter application ... upgrade` might fail
|
|
75
|
+
UPGRADE_RESTRICTION_CODES = {
|
|
76
|
+
CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION,
|
|
77
|
+
CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES,
|
|
78
|
+
ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
|
|
79
|
+
NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
|
|
80
|
+
APPLICATION_NO_LONGER_AVAILABLE,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})
|
|
5
84
|
|
|
6
85
|
|
|
7
86
|
class ApplicationEntity(EntityBase[ApplicationEntityModel]):
|
|
@@ -9,4 +88,610 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
|
|
|
9
88
|
A Native App application object, created from an application package.
|
|
10
89
|
"""
|
|
11
90
|
|
|
12
|
-
|
|
91
|
+
def action_deploy(
|
|
92
|
+
self,
|
|
93
|
+
ctx: ActionContext,
|
|
94
|
+
from_release_directive: bool,
|
|
95
|
+
prune: bool,
|
|
96
|
+
recursive: bool,
|
|
97
|
+
paths: List[Path],
|
|
98
|
+
validate: bool = ValidateOption,
|
|
99
|
+
stage_fqn: Optional[str] = None,
|
|
100
|
+
interactive: bool = InteractiveOption,
|
|
101
|
+
version: Optional[str] = None,
|
|
102
|
+
patch: Optional[int] = None,
|
|
103
|
+
force: Optional[bool] = ForceOption,
|
|
104
|
+
*args,
|
|
105
|
+
**kwargs,
|
|
106
|
+
):
|
|
107
|
+
model = self._entity_model
|
|
108
|
+
app_name = model.fqn.identifier
|
|
109
|
+
debug_mode = model.debug
|
|
110
|
+
if model.meta:
|
|
111
|
+
app_role = getattr(model.meta, "role", ctx.default_role)
|
|
112
|
+
app_warehouse = getattr(model.meta, "warehouse", ctx.default_warehouse)
|
|
113
|
+
post_deploy_hooks = getattr(model.meta, "post_deploy", None)
|
|
114
|
+
else:
|
|
115
|
+
app_role = ctx.default_role
|
|
116
|
+
app_warehouse = ctx.default_warehouse
|
|
117
|
+
post_deploy_hooks = None
|
|
118
|
+
|
|
119
|
+
package_entity: ApplicationPackageEntity = ctx.get_entity(model.from_.target)
|
|
120
|
+
package_model: ApplicationPackageEntityModel = (
|
|
121
|
+
package_entity._entity_model # noqa: SLF001
|
|
122
|
+
)
|
|
123
|
+
package_name = package_model.fqn.identifier
|
|
124
|
+
if package_model.meta and package_model.meta.role:
|
|
125
|
+
package_role = package_model.meta.role
|
|
126
|
+
else:
|
|
127
|
+
package_role = ctx.default_role
|
|
128
|
+
|
|
129
|
+
if not stage_fqn:
|
|
130
|
+
stage_fqn = f"{package_name}.{package_model.stage}"
|
|
131
|
+
stage_schema = extract_schema(stage_fqn)
|
|
132
|
+
|
|
133
|
+
is_interactive = False
|
|
134
|
+
if force:
|
|
135
|
+
policy = AllowAlwaysPolicy()
|
|
136
|
+
elif interactive:
|
|
137
|
+
is_interactive = True
|
|
138
|
+
policy = AskAlwaysPolicy()
|
|
139
|
+
else:
|
|
140
|
+
policy = DenyAlwaysPolicy()
|
|
141
|
+
|
|
142
|
+
def deploy_package():
|
|
143
|
+
package_entity.action_deploy(
|
|
144
|
+
ctx=ctx,
|
|
145
|
+
prune=True,
|
|
146
|
+
recursive=True,
|
|
147
|
+
paths=[],
|
|
148
|
+
validate=validate,
|
|
149
|
+
stage_fqn=stage_fqn,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self.deploy(
|
|
153
|
+
console=ctx.console,
|
|
154
|
+
project_root=ctx.project_root,
|
|
155
|
+
app_name=app_name,
|
|
156
|
+
app_role=app_role,
|
|
157
|
+
app_warehouse=app_warehouse,
|
|
158
|
+
package_name=package_name,
|
|
159
|
+
package_role=package_role,
|
|
160
|
+
stage_schema=stage_schema,
|
|
161
|
+
stage_fqn=stage_fqn,
|
|
162
|
+
debug_mode=debug_mode,
|
|
163
|
+
validate=validate,
|
|
164
|
+
from_release_directive=from_release_directive,
|
|
165
|
+
is_interactive=is_interactive,
|
|
166
|
+
policy=policy,
|
|
167
|
+
version=version,
|
|
168
|
+
patch=patch,
|
|
169
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
170
|
+
deploy_package=deploy_package,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def action_drop(
|
|
174
|
+
self,
|
|
175
|
+
ctx: ActionContext,
|
|
176
|
+
interactive: bool,
|
|
177
|
+
force_drop: bool = False,
|
|
178
|
+
cascade: Optional[bool] = None,
|
|
179
|
+
*args,
|
|
180
|
+
**kwargs,
|
|
181
|
+
):
|
|
182
|
+
model = self._entity_model
|
|
183
|
+
app_name = model.fqn.identifier
|
|
184
|
+
if model.meta and model.meta.role:
|
|
185
|
+
app_role = model.meta.role
|
|
186
|
+
else:
|
|
187
|
+
app_role = ctx.default_role
|
|
188
|
+
self.drop(
|
|
189
|
+
console=ctx.console,
|
|
190
|
+
app_name=app_name,
|
|
191
|
+
app_role=app_role,
|
|
192
|
+
auto_yes=force_drop,
|
|
193
|
+
interactive=interactive,
|
|
194
|
+
cascade=cascade,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def drop(
|
|
199
|
+
cls,
|
|
200
|
+
console: AbstractConsole,
|
|
201
|
+
app_name: str,
|
|
202
|
+
app_role: str,
|
|
203
|
+
auto_yes: bool,
|
|
204
|
+
interactive: bool = False,
|
|
205
|
+
cascade: Optional[bool] = None,
|
|
206
|
+
):
|
|
207
|
+
"""
|
|
208
|
+
Attempts to drop the application object if all validations and user prompts allow so.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
needs_confirm = True
|
|
212
|
+
|
|
213
|
+
# 1. If existing application is not found, exit gracefully
|
|
214
|
+
show_obj_row = cls.get_existing_app_info(
|
|
215
|
+
app_name=app_name,
|
|
216
|
+
app_role=app_role,
|
|
217
|
+
)
|
|
218
|
+
if show_obj_row is None:
|
|
219
|
+
console.warning(
|
|
220
|
+
f"Role {app_role} does not own any application object with the name {app_name}, or the application object does not exist."
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# 2. Check for the right owner
|
|
225
|
+
ensure_correct_owner(row=show_obj_row, role=app_role, obj_name=app_name)
|
|
226
|
+
|
|
227
|
+
# 3. Check if created by the Snowflake CLI
|
|
228
|
+
row_comment = show_obj_row[COMMENT_COL]
|
|
229
|
+
if row_comment not in ALLOWED_SPECIAL_COMMENTS and needs_confirmation(
|
|
230
|
+
needs_confirm, auto_yes
|
|
231
|
+
):
|
|
232
|
+
should_drop_object = typer.confirm(
|
|
233
|
+
dedent(
|
|
234
|
+
f"""\
|
|
235
|
+
Application object {app_name} was not created by Snowflake CLI.
|
|
236
|
+
Application object details:
|
|
237
|
+
Name: {app_name}
|
|
238
|
+
Created on: {show_obj_row["created_on"]}
|
|
239
|
+
Source: {show_obj_row["source"]}
|
|
240
|
+
Owner: {show_obj_row[OWNER_COL]}
|
|
241
|
+
Comment: {show_obj_row[COMMENT_COL]}
|
|
242
|
+
Version: {show_obj_row["version"]}
|
|
243
|
+
Patch: {show_obj_row["patch"]}
|
|
244
|
+
Are you sure you want to drop it?
|
|
245
|
+
"""
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
if not should_drop_object:
|
|
249
|
+
console.message(f"Did not drop application object {app_name}.")
|
|
250
|
+
# The user desires to keep the app, therefore we can't proceed since it would
|
|
251
|
+
# leave behind an orphan app when we get to dropping the package
|
|
252
|
+
raise typer.Abort()
|
|
253
|
+
|
|
254
|
+
# 4. Check for application objects owned by the application
|
|
255
|
+
# This query will fail if the application package has already been dropped, so handle this case gracefully
|
|
256
|
+
has_objects_to_drop = False
|
|
257
|
+
message_prefix = ""
|
|
258
|
+
cascade_true_message = ""
|
|
259
|
+
cascade_false_message = ""
|
|
260
|
+
interactive_prompt = ""
|
|
261
|
+
non_interactive_abort = ""
|
|
262
|
+
try:
|
|
263
|
+
if application_objects := cls.get_objects_owned_by_application(
|
|
264
|
+
app_name=app_name,
|
|
265
|
+
app_role=app_role,
|
|
266
|
+
):
|
|
267
|
+
has_objects_to_drop = True
|
|
268
|
+
message_prefix = (
|
|
269
|
+
f"The following objects are owned by application {app_name}"
|
|
270
|
+
)
|
|
271
|
+
cascade_true_message = f"{message_prefix} and will be dropped:"
|
|
272
|
+
cascade_false_message = f"{message_prefix} and will NOT be dropped:"
|
|
273
|
+
interactive_prompt = "Would you like to drop these objects in addition to the application? [y/n/ABORT]"
|
|
274
|
+
non_interactive_abort = "Re-run teardown again with --cascade or --no-cascade to specify whether these objects should be dropped along with the application"
|
|
275
|
+
except ProgrammingError as e:
|
|
276
|
+
if e.errno != APPLICATION_NO_LONGER_AVAILABLE:
|
|
277
|
+
raise
|
|
278
|
+
application_objects = []
|
|
279
|
+
message_prefix = (
|
|
280
|
+
f"Could not determine which objects are owned by application {app_name}"
|
|
281
|
+
)
|
|
282
|
+
has_objects_to_drop = True # potentially, but we don't know what they are
|
|
283
|
+
cascade_true_message = (
|
|
284
|
+
f"{message_prefix}, an unknown number of objects will be dropped."
|
|
285
|
+
)
|
|
286
|
+
cascade_false_message = f"{message_prefix}, they will NOT be dropped."
|
|
287
|
+
interactive_prompt = f"Would you like to drop an unknown set of objects in addition to the application? [y/n/ABORT]"
|
|
288
|
+
non_interactive_abort = f"Re-run teardown again with --cascade or --no-cascade to specify whether any objects should be dropped along with the application."
|
|
289
|
+
|
|
290
|
+
if has_objects_to_drop:
|
|
291
|
+
if cascade is True:
|
|
292
|
+
# If the user explicitly passed the --cascade flag
|
|
293
|
+
console.message(cascade_true_message)
|
|
294
|
+
with console.indented():
|
|
295
|
+
for obj in application_objects:
|
|
296
|
+
console.message(cls.application_object_to_str(obj))
|
|
297
|
+
elif cascade is False:
|
|
298
|
+
# If the user explicitly passed the --no-cascade flag
|
|
299
|
+
console.message(cascade_false_message)
|
|
300
|
+
with console.indented():
|
|
301
|
+
for obj in application_objects:
|
|
302
|
+
console.message(cls.application_object_to_str(obj))
|
|
303
|
+
elif interactive:
|
|
304
|
+
# If the user didn't pass any cascade flag and the session is interactive
|
|
305
|
+
console.message(message_prefix)
|
|
306
|
+
with console.indented():
|
|
307
|
+
for obj in application_objects:
|
|
308
|
+
console.message(cls.application_object_to_str(obj))
|
|
309
|
+
user_response = typer.prompt(
|
|
310
|
+
interactive_prompt,
|
|
311
|
+
show_default=False,
|
|
312
|
+
default="ABORT",
|
|
313
|
+
).lower()
|
|
314
|
+
if user_response in ["y", "yes"]:
|
|
315
|
+
cascade = True
|
|
316
|
+
elif user_response in ["n", "no"]:
|
|
317
|
+
cascade = False
|
|
318
|
+
else:
|
|
319
|
+
raise typer.Abort()
|
|
320
|
+
else:
|
|
321
|
+
# Else abort since we don't know what to do and can't ask the user
|
|
322
|
+
console.message(message_prefix)
|
|
323
|
+
with console.indented():
|
|
324
|
+
for obj in application_objects:
|
|
325
|
+
console.message(cls.application_object_to_str(obj))
|
|
326
|
+
console.message(non_interactive_abort)
|
|
327
|
+
raise typer.Abort()
|
|
328
|
+
elif cascade is None:
|
|
329
|
+
# If there's nothing to drop, set cascade to an explicit False value
|
|
330
|
+
cascade = False
|
|
331
|
+
|
|
332
|
+
# 5. All validations have passed, drop object
|
|
333
|
+
drop_generic_object(
|
|
334
|
+
console=console,
|
|
335
|
+
object_type="application",
|
|
336
|
+
object_name=app_name,
|
|
337
|
+
role=app_role,
|
|
338
|
+
cascade=cascade,
|
|
339
|
+
)
|
|
340
|
+
return # The application object was successfully dropped, therefore exit gracefully
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def get_objects_owned_by_application(
|
|
344
|
+
app_name: str,
|
|
345
|
+
app_role: str,
|
|
346
|
+
) -> List[ApplicationOwnedObject]:
|
|
347
|
+
"""
|
|
348
|
+
Returns all application objects owned by this application.
|
|
349
|
+
"""
|
|
350
|
+
sql_executor = get_sql_executor()
|
|
351
|
+
with sql_executor.use_role(app_role):
|
|
352
|
+
results = sql_executor.execute_query(
|
|
353
|
+
f"show objects owned by application {app_name}"
|
|
354
|
+
).fetchall()
|
|
355
|
+
return [{"name": row[1], "type": row[2]} for row in results]
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def application_objects_to_str(
|
|
359
|
+
cls, application_objects: list[ApplicationOwnedObject]
|
|
360
|
+
) -> str:
|
|
361
|
+
"""
|
|
362
|
+
Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
|
|
363
|
+
(COMPUTE_POOL) POOL_NAME
|
|
364
|
+
(DATABASE) DB_NAME
|
|
365
|
+
(SCHEMA) DB_NAME.PUBLIC
|
|
366
|
+
...
|
|
367
|
+
"""
|
|
368
|
+
return "\n".join(
|
|
369
|
+
[cls.application_object_to_str(obj) for obj in application_objects]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def application_object_to_str(obj: ApplicationOwnedObject) -> str:
|
|
374
|
+
return f"({obj['type']}) {obj['name']}"
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def deploy(
|
|
378
|
+
cls,
|
|
379
|
+
console: AbstractConsole,
|
|
380
|
+
project_root: Path,
|
|
381
|
+
app_name: str,
|
|
382
|
+
app_role: str,
|
|
383
|
+
app_warehouse: str,
|
|
384
|
+
package_name: str,
|
|
385
|
+
package_role: str,
|
|
386
|
+
stage_schema: str,
|
|
387
|
+
stage_fqn: str,
|
|
388
|
+
debug_mode: bool,
|
|
389
|
+
validate: bool,
|
|
390
|
+
from_release_directive: bool,
|
|
391
|
+
is_interactive: bool,
|
|
392
|
+
policy: PolicyBase,
|
|
393
|
+
deploy_package: Callable,
|
|
394
|
+
version: Optional[str] = None,
|
|
395
|
+
patch: Optional[int] = None,
|
|
396
|
+
post_deploy_hooks: Optional[List[PostDeployHook]] = None,
|
|
397
|
+
drop_application_before_upgrade: Optional[Callable] = None,
|
|
398
|
+
):
|
|
399
|
+
"""
|
|
400
|
+
Create or upgrade the application object using the given strategy
|
|
401
|
+
(unversioned dev, versioned dev, or same-account release directive).
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
# same-account release directive
|
|
405
|
+
if from_release_directive:
|
|
406
|
+
cls.create_or_upgrade_app(
|
|
407
|
+
console=console,
|
|
408
|
+
project_root=project_root,
|
|
409
|
+
package_name=package_name,
|
|
410
|
+
package_role=package_role,
|
|
411
|
+
app_name=app_name,
|
|
412
|
+
app_role=app_role,
|
|
413
|
+
app_warehouse=app_warehouse,
|
|
414
|
+
stage_schema=stage_schema,
|
|
415
|
+
stage_fqn=stage_fqn,
|
|
416
|
+
debug_mode=debug_mode,
|
|
417
|
+
policy=policy,
|
|
418
|
+
install_method=SameAccountInstallMethod.release_directive(),
|
|
419
|
+
is_interactive=is_interactive,
|
|
420
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
421
|
+
drop_application_before_upgrade=drop_application_before_upgrade,
|
|
422
|
+
)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# versioned dev
|
|
426
|
+
if version:
|
|
427
|
+
try:
|
|
428
|
+
version_exists = cls.get_existing_version_info(
|
|
429
|
+
version=version,
|
|
430
|
+
package_name=package_name,
|
|
431
|
+
package_role=package_role,
|
|
432
|
+
)
|
|
433
|
+
if not version_exists:
|
|
434
|
+
raise UsageError(
|
|
435
|
+
f"Application package {package_name} does not have any version {version} defined. Use 'snow app version create' to define a version in the application package first."
|
|
436
|
+
)
|
|
437
|
+
except ApplicationPackageDoesNotExistError as app_err:
|
|
438
|
+
raise UsageError(
|
|
439
|
+
f"Application package {package_name} does not exist. Use 'snow app version create' to first create an application package and then define a version in it."
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
cls.create_or_upgrade_app(
|
|
443
|
+
console=console,
|
|
444
|
+
project_root=project_root,
|
|
445
|
+
package_name=package_name,
|
|
446
|
+
package_role=package_role,
|
|
447
|
+
app_name=app_name,
|
|
448
|
+
app_role=app_role,
|
|
449
|
+
app_warehouse=app_warehouse,
|
|
450
|
+
stage_schema=stage_schema,
|
|
451
|
+
stage_fqn=stage_fqn,
|
|
452
|
+
debug_mode=debug_mode,
|
|
453
|
+
policy=policy,
|
|
454
|
+
install_method=SameAccountInstallMethod.versioned_dev(version, patch),
|
|
455
|
+
is_interactive=is_interactive,
|
|
456
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
457
|
+
drop_application_before_upgrade=drop_application_before_upgrade,
|
|
458
|
+
)
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
# unversioned dev
|
|
462
|
+
deploy_package()
|
|
463
|
+
cls.create_or_upgrade_app(
|
|
464
|
+
console=console,
|
|
465
|
+
project_root=project_root,
|
|
466
|
+
package_name=package_name,
|
|
467
|
+
package_role=package_role,
|
|
468
|
+
app_name=app_name,
|
|
469
|
+
app_role=app_role,
|
|
470
|
+
app_warehouse=app_warehouse,
|
|
471
|
+
stage_schema=stage_schema,
|
|
472
|
+
stage_fqn=stage_fqn,
|
|
473
|
+
debug_mode=debug_mode,
|
|
474
|
+
policy=policy,
|
|
475
|
+
install_method=SameAccountInstallMethod.unversioned_dev(),
|
|
476
|
+
is_interactive=is_interactive,
|
|
477
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
478
|
+
drop_application_before_upgrade=drop_application_before_upgrade,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
@classmethod
|
|
482
|
+
def create_or_upgrade_app(
|
|
483
|
+
cls,
|
|
484
|
+
console: AbstractConsole,
|
|
485
|
+
project_root: Path,
|
|
486
|
+
package_name: str,
|
|
487
|
+
package_role: str,
|
|
488
|
+
app_name: str,
|
|
489
|
+
app_role: str,
|
|
490
|
+
app_warehouse: Optional[str],
|
|
491
|
+
stage_schema: Optional[str],
|
|
492
|
+
stage_fqn: str,
|
|
493
|
+
debug_mode: bool,
|
|
494
|
+
policy: PolicyBase,
|
|
495
|
+
install_method: SameAccountInstallMethod,
|
|
496
|
+
is_interactive: bool = False,
|
|
497
|
+
post_deploy_hooks: Optional[List[PostDeployHook]] = None,
|
|
498
|
+
drop_application_before_upgrade: Optional[Callable] = None,
|
|
499
|
+
):
|
|
500
|
+
sql_executor = get_sql_executor()
|
|
501
|
+
with sql_executor.use_role(app_role):
|
|
502
|
+
|
|
503
|
+
# 1. Need to use a warehouse to create an application object
|
|
504
|
+
with sql_executor.use_warehouse(app_warehouse):
|
|
505
|
+
|
|
506
|
+
# 2. Check for an existing application by the same name
|
|
507
|
+
show_app_row = cls.get_existing_app_info(
|
|
508
|
+
app_name=app_name,
|
|
509
|
+
app_role=app_role,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# 3. If existing application is found, perform a few validations and upgrade the application object.
|
|
513
|
+
if show_app_row:
|
|
514
|
+
|
|
515
|
+
install_method.ensure_app_usable(
|
|
516
|
+
app_name=app_name,
|
|
517
|
+
app_role=app_role,
|
|
518
|
+
show_app_row=show_app_row,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# If all the above checks are in order, proceed to upgrade
|
|
522
|
+
try:
|
|
523
|
+
console.step(
|
|
524
|
+
f"Upgrading existing application object {app_name}."
|
|
525
|
+
)
|
|
526
|
+
using_clause = install_method.using_clause(stage_fqn)
|
|
527
|
+
upgrade_cursor = sql_executor.execute_query(
|
|
528
|
+
f"alter application {app_name} upgrade {using_clause}",
|
|
529
|
+
)
|
|
530
|
+
print_messages(console, upgrade_cursor)
|
|
531
|
+
|
|
532
|
+
if install_method.is_dev_mode:
|
|
533
|
+
# if debug_mode is present (controlled), ensure it is up-to-date
|
|
534
|
+
if debug_mode is not None:
|
|
535
|
+
sql_executor.execute_query(
|
|
536
|
+
f"alter application {app_name} set debug_mode = {debug_mode}"
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# hooks always executed after a create or upgrade
|
|
540
|
+
if post_deploy_hooks:
|
|
541
|
+
cls.execute_post_deploy_hooks(
|
|
542
|
+
console=console,
|
|
543
|
+
project_root=project_root,
|
|
544
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
545
|
+
app_name=app_name,
|
|
546
|
+
app_warehouse=app_warehouse,
|
|
547
|
+
)
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
except ProgrammingError as err:
|
|
551
|
+
if err.errno not in UPGRADE_RESTRICTION_CODES:
|
|
552
|
+
generic_sql_error_handler(err=err)
|
|
553
|
+
else: # The existing application object was created from a different process.
|
|
554
|
+
console.warning(err.msg)
|
|
555
|
+
# TODO Drop the entity here instead of taking a callback once action_drop() is implemented
|
|
556
|
+
if drop_application_before_upgrade:
|
|
557
|
+
drop_application_before_upgrade()
|
|
558
|
+
else:
|
|
559
|
+
raise NotImplementedError
|
|
560
|
+
|
|
561
|
+
# 4. With no (more) existing application objects, create an application object using the release directives
|
|
562
|
+
console.step(f"Creating new application object {app_name} in account.")
|
|
563
|
+
|
|
564
|
+
if app_role != package_role:
|
|
565
|
+
with sql_executor.use_role(package_role):
|
|
566
|
+
sql_executor.execute_query(
|
|
567
|
+
f"grant install, develop on application package {package_name} to role {app_role}"
|
|
568
|
+
)
|
|
569
|
+
sql_executor.execute_query(
|
|
570
|
+
f"grant usage on schema {package_name}.{stage_schema} to role {app_role}"
|
|
571
|
+
)
|
|
572
|
+
sql_executor.execute_query(
|
|
573
|
+
f"grant read on stage {stage_fqn} to role {app_role}"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
# by default, applications are created in debug mode when possible;
|
|
578
|
+
# this can be overridden in the project definition
|
|
579
|
+
debug_mode_clause = ""
|
|
580
|
+
if install_method.is_dev_mode:
|
|
581
|
+
initial_debug_mode = (
|
|
582
|
+
debug_mode if debug_mode is not None else True
|
|
583
|
+
)
|
|
584
|
+
debug_mode_clause = f"debug_mode = {initial_debug_mode}"
|
|
585
|
+
|
|
586
|
+
using_clause = install_method.using_clause(stage_fqn)
|
|
587
|
+
create_cursor = sql_executor.execute_query(
|
|
588
|
+
dedent(
|
|
589
|
+
f"""\
|
|
590
|
+
create application {app_name}
|
|
591
|
+
from application package {package_name} {using_clause} {debug_mode_clause}
|
|
592
|
+
comment = {SPECIAL_COMMENT}
|
|
593
|
+
"""
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
print_messages(console, create_cursor)
|
|
597
|
+
|
|
598
|
+
# hooks always executed after a create or upgrade
|
|
599
|
+
if post_deploy_hooks:
|
|
600
|
+
cls.execute_post_deploy_hooks(
|
|
601
|
+
console=console,
|
|
602
|
+
project_root=project_root,
|
|
603
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
604
|
+
app_name=app_name,
|
|
605
|
+
app_warehouse=app_warehouse,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
except ProgrammingError as err:
|
|
609
|
+
generic_sql_error_handler(err)
|
|
610
|
+
|
|
611
|
+
@classmethod
|
|
612
|
+
def execute_post_deploy_hooks(
|
|
613
|
+
cls,
|
|
614
|
+
console: AbstractConsole,
|
|
615
|
+
project_root: Path,
|
|
616
|
+
post_deploy_hooks: Optional[List[PostDeployHook]],
|
|
617
|
+
app_name: str,
|
|
618
|
+
app_warehouse: Optional[str],
|
|
619
|
+
):
|
|
620
|
+
with cls.use_application_warehouse(app_warehouse):
|
|
621
|
+
execute_post_deploy_hooks(
|
|
622
|
+
console=console,
|
|
623
|
+
project_root=project_root,
|
|
624
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
625
|
+
deployed_object_type="application",
|
|
626
|
+
database_name=app_name,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
@staticmethod
|
|
630
|
+
@contextmanager
|
|
631
|
+
def use_application_warehouse(
|
|
632
|
+
app_warehouse: Optional[str],
|
|
633
|
+
):
|
|
634
|
+
if app_warehouse:
|
|
635
|
+
with get_sql_executor().use_warehouse(app_warehouse):
|
|
636
|
+
yield
|
|
637
|
+
else:
|
|
638
|
+
raise ClickException(
|
|
639
|
+
dedent(
|
|
640
|
+
f"""\
|
|
641
|
+
Application warehouse cannot be empty.
|
|
642
|
+
Please provide a value for it in your connection information or your project definition file.
|
|
643
|
+
"""
|
|
644
|
+
)
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
@staticmethod
|
|
648
|
+
def get_existing_app_info(
|
|
649
|
+
app_name: str,
|
|
650
|
+
app_role: str,
|
|
651
|
+
) -> Optional[dict]:
|
|
652
|
+
"""
|
|
653
|
+
Check for an existing application object by the same name as in project definition, in account.
|
|
654
|
+
It executes a 'show applications like' query and returns the result as single row, if one exists.
|
|
655
|
+
"""
|
|
656
|
+
sql_executor = get_sql_executor()
|
|
657
|
+
with sql_executor.use_role(app_role):
|
|
658
|
+
return sql_executor.show_specific_object(
|
|
659
|
+
"applications", app_name, name_col=NAME_COL
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
@staticmethod
|
|
663
|
+
def get_existing_version_info(
|
|
664
|
+
version: str,
|
|
665
|
+
package_name: str,
|
|
666
|
+
package_role: str,
|
|
667
|
+
) -> Optional[dict]:
|
|
668
|
+
"""
|
|
669
|
+
Get the latest patch on an existing version by name in the application package.
|
|
670
|
+
Executes 'show versions like ... in application package' query and returns
|
|
671
|
+
the latest patch in the version as a single row, if one exists. Otherwise,
|
|
672
|
+
returns None.
|
|
673
|
+
"""
|
|
674
|
+
sql_executor = get_sql_executor()
|
|
675
|
+
with sql_executor.use_role(package_role):
|
|
676
|
+
try:
|
|
677
|
+
query = f"show versions like {identifier_to_show_like_pattern(version)} in application package {package_name}"
|
|
678
|
+
cursor = sql_executor.execute_query(query, cursor_class=DictCursor)
|
|
679
|
+
|
|
680
|
+
if cursor.rowcount is None:
|
|
681
|
+
raise SnowflakeSQLExecutionError(query)
|
|
682
|
+
|
|
683
|
+
matching_rows = find_all_rows(
|
|
684
|
+
cursor, lambda row: row[VERSION_COL] == unquote_identifier(version)
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
if not matching_rows:
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
return max(matching_rows, key=lambda row: row[PATCH_COL])
|
|
691
|
+
|
|
692
|
+
except ProgrammingError as err:
|
|
693
|
+
if err.msg.__contains__("does not exist or not authorized"):
|
|
694
|
+
raise ApplicationPackageDoesNotExistError(package_name)
|
|
695
|
+
else:
|
|
696
|
+
generic_sql_error_handler(err=err, role=package_role)
|
|
697
|
+
return None
|