snowflake-cli 3.2.2__py3-none-any.whl → 3.3.0__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/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
- snowflake/cli/_plugins/nativeapp/commands.py +92 -2
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
- snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
- snowflake/cli/_plugins/notebook/manager.py +4 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +134 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/manager.py +12 -4
- snowflake/cli/_plugins/streamlit/manager.py +8 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
- snowflake/cli/_plugins/workspace/commands.py +3 -2
- snowflake/cli/_plugins/workspace/manager.py +8 -4
- snowflake/cli/api/cli_global_context.py +22 -1
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +9 -1
- snowflake/cli/api/entities/common.py +85 -0
- snowflake/cli/api/entities/utils.py +9 -8
- snowflake/cli/api/errno.py +60 -3
- snowflake/cli/api/feature_flags.py +20 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +1 -2
- snowflake/cli/api/project/schemas/project_definition.py +27 -6
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +12 -12
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +55 -50
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import re
|
|
6
|
+
from datetime import datetime
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from textwrap import dedent
|
|
7
|
-
from typing import List, Literal, Optional, Union
|
|
9
|
+
from typing import Any, List, Literal, Optional, Set, Union
|
|
8
10
|
|
|
9
11
|
import typer
|
|
10
|
-
from click import BadOptionUsage, ClickException
|
|
12
|
+
from click import BadOptionUsage, ClickException, UsageError
|
|
11
13
|
from pydantic import Field, field_validator
|
|
14
|
+
from snowflake.cli._plugins.connection.util import UIParameter
|
|
12
15
|
from snowflake.cli._plugins.nativeapp.artifacts import (
|
|
13
16
|
BundleMap,
|
|
14
17
|
VersionInfo,
|
|
15
18
|
build_bundle,
|
|
19
|
+
find_setup_script_file,
|
|
16
20
|
find_version_info_in_manifest_file,
|
|
17
21
|
)
|
|
18
22
|
from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
|
|
@@ -20,14 +24,19 @@ from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler
|
|
|
20
24
|
from snowflake.cli._plugins.nativeapp.constants import (
|
|
21
25
|
ALLOWED_SPECIAL_COMMENTS,
|
|
22
26
|
COMMENT_COL,
|
|
27
|
+
DEFAULT_CHANNEL,
|
|
28
|
+
DEFAULT_DIRECTIVE,
|
|
23
29
|
EXTERNAL_DISTRIBUTION,
|
|
24
30
|
INTERNAL_DISTRIBUTION,
|
|
31
|
+
MAX_VERSIONS_IN_RELEASE_CHANNEL,
|
|
25
32
|
NAME_COL,
|
|
26
33
|
OWNER_COL,
|
|
27
34
|
PATCH_COL,
|
|
28
|
-
SPECIAL_COMMENT,
|
|
29
35
|
VERSION_COL,
|
|
30
36
|
)
|
|
37
|
+
from snowflake.cli._plugins.nativeapp.entities.application_package_child_interface import (
|
|
38
|
+
ApplicationPackageChildInterface,
|
|
39
|
+
)
|
|
31
40
|
from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
32
41
|
ApplicationPackageAlreadyExistsError,
|
|
33
42
|
ApplicationPackageDoesNotExistError,
|
|
@@ -35,6 +44,7 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
|
35
44
|
ObjectPropertyNotFoundError,
|
|
36
45
|
SetupScriptFailedValidation,
|
|
37
46
|
)
|
|
47
|
+
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
|
|
38
48
|
from snowflake.cli._plugins.nativeapp.policy import (
|
|
39
49
|
AllowAlwaysPolicy,
|
|
40
50
|
AskAlwaysPolicy,
|
|
@@ -45,11 +55,24 @@ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
|
|
|
45
55
|
from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
|
|
46
56
|
InsufficientPrivilegesError,
|
|
47
57
|
)
|
|
48
|
-
from snowflake.cli._plugins.nativeapp.
|
|
58
|
+
from snowflake.cli._plugins.nativeapp.sf_sql_facade import ReleaseChannel, Version
|
|
59
|
+
from snowflake.cli._plugins.nativeapp.utils import needs_confirmation, sanitize_dir_name
|
|
60
|
+
from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
|
|
61
|
+
FunctionEntityModel,
|
|
62
|
+
ProcedureEntityModel,
|
|
63
|
+
)
|
|
49
64
|
from snowflake.cli._plugins.stage.diff import DiffResult
|
|
50
65
|
from snowflake.cli._plugins.stage.manager import StageManager
|
|
66
|
+
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
67
|
+
StreamlitEntityModel,
|
|
68
|
+
)
|
|
51
69
|
from snowflake.cli._plugins.workspace.context import ActionContext
|
|
52
|
-
from snowflake.cli.api.
|
|
70
|
+
from snowflake.cli.api.cli_global_context import span
|
|
71
|
+
from snowflake.cli.api.entities.common import (
|
|
72
|
+
EntityBase,
|
|
73
|
+
attach_spans_to_entity_actions,
|
|
74
|
+
get_sql_executor,
|
|
75
|
+
)
|
|
53
76
|
from snowflake.cli.api.entities.utils import (
|
|
54
77
|
drop_generic_object,
|
|
55
78
|
execute_post_deploy_hooks,
|
|
@@ -67,14 +90,18 @@ from snowflake.cli.api.project.schemas.entities.common import (
|
|
|
67
90
|
from snowflake.cli.api.project.schemas.updatable_model import (
|
|
68
91
|
DiscriminatorField,
|
|
69
92
|
IdentifierField,
|
|
93
|
+
UpdatableModel,
|
|
70
94
|
)
|
|
71
95
|
from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions
|
|
72
96
|
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
|
|
73
97
|
from snowflake.cli.api.project.util import (
|
|
74
98
|
SCHEMA_AND_NAME,
|
|
99
|
+
VALID_IDENTIFIER_REGEX,
|
|
75
100
|
append_test_resource_suffix,
|
|
76
101
|
extract_schema,
|
|
77
102
|
identifier_to_show_like_pattern,
|
|
103
|
+
same_identifiers,
|
|
104
|
+
sql_match,
|
|
78
105
|
to_identifier,
|
|
79
106
|
unquote_identifier,
|
|
80
107
|
)
|
|
@@ -82,6 +109,43 @@ from snowflake.cli.api.utils.cursor import find_all_rows
|
|
|
82
109
|
from snowflake.connector import DictCursor, ProgrammingError
|
|
83
110
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
84
111
|
|
|
112
|
+
ApplicationPackageChildrenTypes = (
|
|
113
|
+
StreamlitEntityModel | FunctionEntityModel | ProcedureEntityModel
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ApplicationPackageChildIdentifier(UpdatableModel):
|
|
118
|
+
schema_: Optional[str] = Field(
|
|
119
|
+
title="Child entity schema", alias="schema", default=None
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class EnsureUsableByField(UpdatableModel):
|
|
124
|
+
application_roles: Optional[Union[str, Set[str]]] = Field(
|
|
125
|
+
title="One or more application roles to be granted with the required privileges",
|
|
126
|
+
default=None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@field_validator("application_roles")
|
|
130
|
+
@classmethod
|
|
131
|
+
def ensure_app_roles_is_a_set(
|
|
132
|
+
cls, application_roles: Optional[Union[str, Set[str]]]
|
|
133
|
+
) -> Optional[Union[Set[str]]]:
|
|
134
|
+
if isinstance(application_roles, str):
|
|
135
|
+
return set([application_roles])
|
|
136
|
+
return application_roles
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ApplicationPackageChildField(UpdatableModel):
|
|
140
|
+
target: str = Field(title="The key of the entity to include in this package")
|
|
141
|
+
ensure_usable_by: Optional[EnsureUsableByField] = Field(
|
|
142
|
+
title="Automatically grant the required privileges on the child object and its schema",
|
|
143
|
+
default=None,
|
|
144
|
+
)
|
|
145
|
+
identifier: ApplicationPackageChildIdentifier = Field(
|
|
146
|
+
title="Entity identifier", default=None
|
|
147
|
+
)
|
|
148
|
+
|
|
85
149
|
|
|
86
150
|
class ApplicationPackageEntityModel(EntityModelBase):
|
|
87
151
|
type: Literal["application package"] = DiscriminatorField() # noqa: A003
|
|
@@ -89,23 +153,27 @@ class ApplicationPackageEntityModel(EntityModelBase):
|
|
|
89
153
|
title="List of paths or file source/destination pairs to add to the deploy root",
|
|
90
154
|
)
|
|
91
155
|
bundle_root: Optional[str] = Field(
|
|
92
|
-
title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored
|
|
156
|
+
title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored",
|
|
93
157
|
default="output/bundle/",
|
|
94
158
|
)
|
|
95
159
|
deploy_root: Optional[str] = Field(
|
|
96
160
|
title="Folder at the root of your project where the build step copies the artifacts",
|
|
97
161
|
default="output/deploy/",
|
|
98
162
|
)
|
|
163
|
+
children_artifacts_dir: Optional[str] = Field(
|
|
164
|
+
title="Folder under deploy_root where the child artifacts will be stored",
|
|
165
|
+
default="_children/",
|
|
166
|
+
)
|
|
99
167
|
generated_root: Optional[str] = Field(
|
|
100
|
-
title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written
|
|
168
|
+
title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written",
|
|
101
169
|
default="__generated/",
|
|
102
170
|
)
|
|
103
171
|
stage: Optional[str] = IdentifierField(
|
|
104
|
-
title="Identifier of the stage that stores the application artifacts
|
|
172
|
+
title="Identifier of the stage that stores the application artifacts",
|
|
105
173
|
default="app_src.stage",
|
|
106
174
|
)
|
|
107
175
|
scratch_stage: Optional[str] = IdentifierField(
|
|
108
|
-
title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI
|
|
176
|
+
title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI",
|
|
109
177
|
default="app_src.stage_snowflake_cli_scratch",
|
|
110
178
|
)
|
|
111
179
|
distribution: Optional[DistributionOptions] = Field(
|
|
@@ -116,6 +184,19 @@ class ApplicationPackageEntityModel(EntityModelBase):
|
|
|
116
184
|
title="Path to manifest.yml. Unused and deprecated starting with Snowflake CLI 3.2",
|
|
117
185
|
default="",
|
|
118
186
|
)
|
|
187
|
+
children: Optional[List[ApplicationPackageChildField]] = Field(
|
|
188
|
+
title="Entities that will be bundled and deployed as part of this application package",
|
|
189
|
+
default=[],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@field_validator("children")
|
|
193
|
+
@classmethod
|
|
194
|
+
def verify_children_behind_flag(
|
|
195
|
+
cls, input_value: Optional[List[ApplicationPackageChildField]]
|
|
196
|
+
) -> Optional[List[ApplicationPackageChildField]]:
|
|
197
|
+
if input_value and not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
|
|
198
|
+
raise AttributeError("Application package children are not supported yet")
|
|
199
|
+
return input_value
|
|
119
200
|
|
|
120
201
|
@field_validator("identifier")
|
|
121
202
|
@classmethod
|
|
@@ -157,6 +238,7 @@ class ApplicationPackageEntityModel(EntityModelBase):
|
|
|
157
238
|
return input_value
|
|
158
239
|
|
|
159
240
|
|
|
241
|
+
@attach_spans_to_entity_actions(entity_name="app_pkg")
|
|
160
242
|
class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
161
243
|
"""
|
|
162
244
|
A Native App application package.
|
|
@@ -170,6 +252,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
170
252
|
def deploy_root(self) -> Path:
|
|
171
253
|
return self.project_root / self._entity_model.deploy_root
|
|
172
254
|
|
|
255
|
+
@property
|
|
256
|
+
def children_artifacts_deploy_root(self) -> Path:
|
|
257
|
+
return self.deploy_root / self._entity_model.children_artifacts_dir
|
|
258
|
+
|
|
173
259
|
@property
|
|
174
260
|
def bundle_root(self) -> Path:
|
|
175
261
|
return self.project_root / self._entity_model.bundle_root
|
|
@@ -208,7 +294,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
208
294
|
return model.meta and model.meta.post_deploy
|
|
209
295
|
|
|
210
296
|
def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
|
|
211
|
-
return self._bundle()
|
|
297
|
+
return self._bundle(action_ctx)
|
|
212
298
|
|
|
213
299
|
def action_deploy(
|
|
214
300
|
self,
|
|
@@ -224,6 +310,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
224
310
|
**kwargs,
|
|
225
311
|
):
|
|
226
312
|
return self._deploy(
|
|
313
|
+
action_ctx=action_ctx,
|
|
227
314
|
bundle_map=None,
|
|
228
315
|
prune=prune,
|
|
229
316
|
recursive=recursive,
|
|
@@ -248,21 +335,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
248
335
|
)
|
|
249
336
|
return
|
|
250
337
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if show_versions_cursor.rowcount > 0:
|
|
261
|
-
# allow dropping a package with versions when --force is set
|
|
262
|
-
if not force_drop:
|
|
263
|
-
raise CouldNotDropApplicationPackageWithVersions(
|
|
264
|
-
"Drop versions first, or use --force to override."
|
|
265
|
-
)
|
|
338
|
+
# 2. Check for versions in the application package
|
|
339
|
+
versions_in_pkg = get_snowflake_facade().show_versions(self.name, self.role)
|
|
340
|
+
if len(versions_in_pkg) > 0:
|
|
341
|
+
# allow dropping a package with versions when --force is set
|
|
342
|
+
if not force_drop:
|
|
343
|
+
raise CouldNotDropApplicationPackageWithVersions(
|
|
344
|
+
"Drop versions first, or use --force to override."
|
|
345
|
+
)
|
|
266
346
|
|
|
267
347
|
# 3. Check distribution of the existing application package
|
|
268
348
|
actual_distribution = self.get_app_pkg_distribution_in_snowflake()
|
|
@@ -323,6 +403,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
323
403
|
**kwargs,
|
|
324
404
|
):
|
|
325
405
|
self.validate_setup_script(
|
|
406
|
+
action_ctx=action_ctx,
|
|
326
407
|
use_scratch_stage=use_scratch_stage,
|
|
327
408
|
interactive=interactive,
|
|
328
409
|
force=force,
|
|
@@ -336,15 +417,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
336
417
|
Get all existing versions, if defined, for an application package.
|
|
337
418
|
It executes a 'show versions in application package' query and returns all the results.
|
|
338
419
|
"""
|
|
339
|
-
|
|
340
|
-
with sql_executor.use_role(self.role):
|
|
341
|
-
show_obj_query = f"show versions in application package {self.name}"
|
|
342
|
-
show_obj_cursor = sql_executor.execute_query(show_obj_query)
|
|
343
|
-
|
|
344
|
-
if show_obj_cursor.rowcount is None:
|
|
345
|
-
raise SnowflakeSQLExecutionError(show_obj_query)
|
|
346
|
-
|
|
347
|
-
return show_obj_cursor
|
|
420
|
+
return get_snowflake_facade().show_versions(self.name, self.role)
|
|
348
421
|
|
|
349
422
|
def action_version_create(
|
|
350
423
|
self,
|
|
@@ -355,9 +428,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
355
428
|
skip_git_check: bool,
|
|
356
429
|
interactive: bool,
|
|
357
430
|
force: bool,
|
|
431
|
+
from_stage: Optional[bool],
|
|
358
432
|
*args,
|
|
359
433
|
**kwargs,
|
|
360
|
-
):
|
|
434
|
+
) -> VersionInfo:
|
|
361
435
|
"""
|
|
362
436
|
Create a version and/or patch for a new or existing application package.
|
|
363
437
|
Always performs a deploy action before creating version or patch.
|
|
@@ -372,12 +446,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
372
446
|
else:
|
|
373
447
|
policy = DenyAlwaysPolicy()
|
|
374
448
|
|
|
375
|
-
|
|
376
|
-
git_policy = DenyAlwaysPolicy()
|
|
377
|
-
else:
|
|
378
|
-
git_policy = AllowAlwaysPolicy()
|
|
379
|
-
|
|
380
|
-
bundle_map = self._bundle()
|
|
449
|
+
bundle_map = self._bundle(action_ctx)
|
|
381
450
|
resolved_version, resolved_patch, resolved_label = self.resolve_version_info(
|
|
382
451
|
version=version,
|
|
383
452
|
patch=patch,
|
|
@@ -387,20 +456,31 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
387
456
|
interactive=interactive,
|
|
388
457
|
)
|
|
389
458
|
|
|
390
|
-
if
|
|
459
|
+
if not skip_git_check:
|
|
391
460
|
self.check_index_changes_in_git_repo(policy=policy, interactive=interactive)
|
|
392
461
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
462
|
+
# if user is asking to create the version from the current stage,
|
|
463
|
+
# then do not re-deploy the artifacts or touch the stage
|
|
464
|
+
if from_stage:
|
|
465
|
+
# verify package exists:
|
|
466
|
+
if not self.get_existing_app_pkg_info():
|
|
467
|
+
raise ClickException(
|
|
468
|
+
"Cannot create version from stage because the application package does not exist yet. "
|
|
469
|
+
"Try removing --from-stage flag or executing `snow app deploy` to deploy the application package first."
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
self._deploy(
|
|
473
|
+
action_ctx=action_ctx,
|
|
474
|
+
bundle_map=bundle_map,
|
|
475
|
+
prune=True,
|
|
476
|
+
recursive=True,
|
|
477
|
+
paths=[],
|
|
478
|
+
print_diff=True,
|
|
479
|
+
validate=True,
|
|
480
|
+
stage_fqn=self.stage_fqn,
|
|
481
|
+
interactive=interactive,
|
|
482
|
+
force=force,
|
|
483
|
+
)
|
|
404
484
|
|
|
405
485
|
# Warn if the version exists in a release directive(s)
|
|
406
486
|
try:
|
|
@@ -440,12 +520,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
440
520
|
# Define a new version in the application package
|
|
441
521
|
if not self.get_existing_version_info(resolved_version):
|
|
442
522
|
self.add_new_version(version=resolved_version, label=resolved_label)
|
|
443
|
-
|
|
523
|
+
# A new version created automatically has patch 0, we do not need to further increment the patch.
|
|
524
|
+
return VersionInfo(resolved_version, 0, resolved_label)
|
|
444
525
|
|
|
445
526
|
# Add a new patch to an existing (old) version
|
|
446
|
-
self.add_new_patch_to_version(
|
|
527
|
+
patch = self.add_new_patch_to_version(
|
|
447
528
|
version=resolved_version, patch=resolved_patch, label=resolved_label
|
|
448
529
|
)
|
|
530
|
+
return VersionInfo(resolved_version, patch, resolved_label)
|
|
449
531
|
|
|
450
532
|
def action_version_drop(
|
|
451
533
|
self,
|
|
@@ -492,7 +574,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
492
574
|
"""
|
|
493
575
|
)
|
|
494
576
|
)
|
|
495
|
-
self._bundle()
|
|
577
|
+
self._bundle(action_ctx)
|
|
496
578
|
version_info = find_version_info_in_manifest_file(self.deploy_root)
|
|
497
579
|
version = version_info.version_name
|
|
498
580
|
if not version:
|
|
@@ -524,20 +606,231 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
524
606
|
raise typer.Exit(1)
|
|
525
607
|
|
|
526
608
|
# Drop the version
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
sql_executor.execute_query(
|
|
531
|
-
f"alter application package {self.name} drop version {version}"
|
|
532
|
-
)
|
|
533
|
-
except ProgrammingError as err:
|
|
534
|
-
raise err # e.g. version is referenced in a release directive(s)
|
|
609
|
+
get_snowflake_facade().drop_version_from_package(
|
|
610
|
+
package_name=self.name, version=version, role=self.role
|
|
611
|
+
)
|
|
535
612
|
|
|
536
613
|
console.message(
|
|
537
614
|
f"Version {version} in application package {self.name} dropped successfully."
|
|
538
615
|
)
|
|
539
616
|
|
|
540
|
-
def
|
|
617
|
+
def _validate_target_accounts(self, accounts: list[str]) -> None:
|
|
618
|
+
"""
|
|
619
|
+
Validates the target accounts provided by the user.
|
|
620
|
+
"""
|
|
621
|
+
for account in accounts:
|
|
622
|
+
if not re.fullmatch(
|
|
623
|
+
f"{VALID_IDENTIFIER_REGEX}\\.{VALID_IDENTIFIER_REGEX}", account
|
|
624
|
+
):
|
|
625
|
+
raise ClickException(
|
|
626
|
+
f"Target account {account} is not in a valid format. Make sure you provide the target account in the format 'org.account'."
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def get_sanitized_release_channel(
|
|
630
|
+
self,
|
|
631
|
+
release_channel: Optional[str],
|
|
632
|
+
available_release_channels: Optional[list[ReleaseChannel]] = None,
|
|
633
|
+
) -> Optional[str]:
|
|
634
|
+
"""
|
|
635
|
+
Sanitize the release channel name provided by the user and validate it against the available release channels.
|
|
636
|
+
|
|
637
|
+
A return value of None indicates that release channels should not be used. Returns None if:
|
|
638
|
+
- Release channel is not provided
|
|
639
|
+
- Release channels are not enabled in the application package and the user provided the default release channel
|
|
640
|
+
"""
|
|
641
|
+
if not release_channel:
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
if available_release_channels is None:
|
|
645
|
+
available_release_channels = get_snowflake_facade().show_release_channels(
|
|
646
|
+
self.name, self.role
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if not available_release_channels and same_identifiers(
|
|
650
|
+
release_channel, DEFAULT_CHANNEL
|
|
651
|
+
):
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
self.validate_release_channel(release_channel, available_release_channels)
|
|
655
|
+
return release_channel
|
|
656
|
+
|
|
657
|
+
def validate_release_channel(
|
|
658
|
+
self,
|
|
659
|
+
release_channel: str,
|
|
660
|
+
available_release_channels: Optional[list[ReleaseChannel]] = None,
|
|
661
|
+
) -> None:
|
|
662
|
+
"""
|
|
663
|
+
Validates the release channel provided by the user and make sure it is a valid release channel for the application package.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
if available_release_channels is None:
|
|
667
|
+
available_release_channels = get_snowflake_facade().show_release_channels(
|
|
668
|
+
self.name, self.role
|
|
669
|
+
)
|
|
670
|
+
if not available_release_channels:
|
|
671
|
+
raise UsageError(
|
|
672
|
+
f"Release channels are not enabled for application package {self.name}."
|
|
673
|
+
)
|
|
674
|
+
for channel in available_release_channels:
|
|
675
|
+
if unquote_identifier(release_channel) == channel["name"]:
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
raise UsageError(
|
|
679
|
+
f"Release channel {release_channel} is not available in application package {self.name}. "
|
|
680
|
+
f"Available release channels are: ({', '.join(channel['name'] for channel in available_release_channels)})."
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def action_release_directive_list(
|
|
684
|
+
self,
|
|
685
|
+
action_ctx: ActionContext,
|
|
686
|
+
release_channel: Optional[str],
|
|
687
|
+
like: str,
|
|
688
|
+
*args,
|
|
689
|
+
**kwargs,
|
|
690
|
+
) -> list[dict[str, Any]]:
|
|
691
|
+
"""
|
|
692
|
+
Get all existing release directives for an application package.
|
|
693
|
+
Limit the results to a specific release channel, if provided.
|
|
694
|
+
|
|
695
|
+
If `like` is provided, only release directives matching the SQL LIKE pattern are listed.
|
|
696
|
+
"""
|
|
697
|
+
release_channel = self.get_sanitized_release_channel(release_channel)
|
|
698
|
+
|
|
699
|
+
release_directives = get_snowflake_facade().show_release_directives(
|
|
700
|
+
package_name=self.name,
|
|
701
|
+
role=self.role,
|
|
702
|
+
release_channel=release_channel,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
return [
|
|
706
|
+
directive
|
|
707
|
+
for directive in release_directives
|
|
708
|
+
if sql_match(pattern=like, value=directive.get("name", ""))
|
|
709
|
+
]
|
|
710
|
+
|
|
711
|
+
def action_release_directive_set(
|
|
712
|
+
self,
|
|
713
|
+
action_ctx: ActionContext,
|
|
714
|
+
version: str,
|
|
715
|
+
patch: int,
|
|
716
|
+
release_directive: str,
|
|
717
|
+
release_channel: str,
|
|
718
|
+
target_accounts: Optional[list[str]],
|
|
719
|
+
*args,
|
|
720
|
+
**kwargs,
|
|
721
|
+
):
|
|
722
|
+
"""
|
|
723
|
+
Sets a release directive to the specified version and patch using the specified release channel.
|
|
724
|
+
Target accounts can only be specified for non-default release directives.
|
|
725
|
+
|
|
726
|
+
For non-default release directives, update the existing release directive if target accounts are not provided.
|
|
727
|
+
"""
|
|
728
|
+
if target_accounts:
|
|
729
|
+
self._validate_target_accounts(target_accounts)
|
|
730
|
+
|
|
731
|
+
if target_accounts and same_identifiers(release_directive, DEFAULT_DIRECTIVE):
|
|
732
|
+
raise BadOptionUsage(
|
|
733
|
+
"target_accounts",
|
|
734
|
+
"Target accounts can only be specified for non-default named release directives.",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
sanitized_release_channel = self.get_sanitized_release_channel(release_channel)
|
|
738
|
+
|
|
739
|
+
get_snowflake_facade().set_release_directive(
|
|
740
|
+
package_name=self.name,
|
|
741
|
+
release_directive=release_directive,
|
|
742
|
+
release_channel=sanitized_release_channel,
|
|
743
|
+
target_accounts=target_accounts,
|
|
744
|
+
version=version,
|
|
745
|
+
patch=patch,
|
|
746
|
+
role=self.role,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
def action_release_directive_unset(
|
|
750
|
+
self,
|
|
751
|
+
action_ctx: ActionContext,
|
|
752
|
+
release_directive: str,
|
|
753
|
+
release_channel: str,
|
|
754
|
+
):
|
|
755
|
+
"""
|
|
756
|
+
Unsets a release directive from the specified release channel.
|
|
757
|
+
"""
|
|
758
|
+
if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
|
|
759
|
+
raise ClickException(
|
|
760
|
+
"Cannot unset default release directive. Please specify a non-default release directive."
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
get_snowflake_facade().unset_release_directive(
|
|
764
|
+
package_name=self.name,
|
|
765
|
+
release_directive=release_directive,
|
|
766
|
+
release_channel=self.get_sanitized_release_channel(release_channel),
|
|
767
|
+
role=self.role,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
def _print_channel_to_console(self, channel: ReleaseChannel) -> None:
|
|
771
|
+
"""
|
|
772
|
+
Prints the release channel details to the console.
|
|
773
|
+
"""
|
|
774
|
+
console = self._workspace_ctx.console
|
|
775
|
+
|
|
776
|
+
console.message(f"""[bold]{channel["name"]}[/bold]""")
|
|
777
|
+
accounts_list: Optional[list[str]] = channel["targets"].get("accounts")
|
|
778
|
+
target_accounts = (
|
|
779
|
+
f"({', '.join(accounts_list)})"
|
|
780
|
+
if accounts_list is not None
|
|
781
|
+
else "ALL ACCOUNTS"
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
formatted_created_on = (
|
|
785
|
+
channel["created_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z")
|
|
786
|
+
if channel["created_on"]
|
|
787
|
+
else ""
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
formatted_updated_on = (
|
|
791
|
+
channel["updated_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z")
|
|
792
|
+
if channel["updated_on"]
|
|
793
|
+
else ""
|
|
794
|
+
)
|
|
795
|
+
with console.indented():
|
|
796
|
+
console.message(f"Description: {channel['description']}")
|
|
797
|
+
console.message(f"Versions: ({', '.join(channel['versions'])})")
|
|
798
|
+
console.message(f"Created on: {formatted_created_on}")
|
|
799
|
+
console.message(f"Updated on: {formatted_updated_on}")
|
|
800
|
+
console.message(f"Target accounts: {target_accounts}")
|
|
801
|
+
|
|
802
|
+
def action_release_channel_list(
|
|
803
|
+
self,
|
|
804
|
+
action_ctx: ActionContext,
|
|
805
|
+
release_channel: Optional[str],
|
|
806
|
+
*args,
|
|
807
|
+
**kwargs,
|
|
808
|
+
) -> list[ReleaseChannel]:
|
|
809
|
+
"""
|
|
810
|
+
Get all existing release channels for an application package.
|
|
811
|
+
If `release_channel` is provided, only the specified release channel is listed.
|
|
812
|
+
"""
|
|
813
|
+
console = self._workspace_ctx.console
|
|
814
|
+
available_channels = get_snowflake_facade().show_release_channels(
|
|
815
|
+
self.name, self.role
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
filtered_channels = [
|
|
819
|
+
channel
|
|
820
|
+
for channel in available_channels
|
|
821
|
+
if release_channel is None
|
|
822
|
+
or unquote_identifier(release_channel) == channel["name"]
|
|
823
|
+
]
|
|
824
|
+
|
|
825
|
+
if not filtered_channels:
|
|
826
|
+
console.message("No release channels found.")
|
|
827
|
+
else:
|
|
828
|
+
for channel in filtered_channels:
|
|
829
|
+
self._print_channel_to_console(channel)
|
|
830
|
+
|
|
831
|
+
return filtered_channels
|
|
832
|
+
|
|
833
|
+
def _bundle(self, action_ctx: ActionContext = None):
|
|
541
834
|
model = self._entity_model
|
|
542
835
|
bundle_map = build_bundle(self.project_root, self.deploy_root, model.artifacts)
|
|
543
836
|
bundle_context = BundleContext(
|
|
@@ -550,10 +843,359 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
550
843
|
)
|
|
551
844
|
compiler = NativeAppCompiler(bundle_context)
|
|
552
845
|
compiler.compile_artifacts()
|
|
846
|
+
|
|
847
|
+
if self._entity_model.children:
|
|
848
|
+
# Bundle children and append their SQL to setup script
|
|
849
|
+
# TODO Consider re-writing the logic below as a processor
|
|
850
|
+
children_sql = self._bundle_children(action_ctx=action_ctx)
|
|
851
|
+
setup_file_path = find_setup_script_file(deploy_root=self.deploy_root)
|
|
852
|
+
with open(setup_file_path, "r", encoding="utf-8") as file:
|
|
853
|
+
existing_setup_script = file.read()
|
|
854
|
+
if setup_file_path.is_symlink():
|
|
855
|
+
setup_file_path.unlink()
|
|
856
|
+
with open(setup_file_path, "w", encoding="utf-8") as file:
|
|
857
|
+
file.write(existing_setup_script)
|
|
858
|
+
file.write("\n-- AUTO GENERATED CHILDREN SECTION\n")
|
|
859
|
+
file.write("\n".join(children_sql))
|
|
860
|
+
file.write("\n")
|
|
861
|
+
|
|
553
862
|
return bundle_map
|
|
554
863
|
|
|
864
|
+
def action_release_channel_add_accounts(
|
|
865
|
+
self,
|
|
866
|
+
action_ctx: ActionContext,
|
|
867
|
+
release_channel: str,
|
|
868
|
+
target_accounts: list[str],
|
|
869
|
+
*args,
|
|
870
|
+
**kwargs,
|
|
871
|
+
):
|
|
872
|
+
"""
|
|
873
|
+
Adds target accounts to a release channel.
|
|
874
|
+
"""
|
|
875
|
+
|
|
876
|
+
if not target_accounts:
|
|
877
|
+
raise ClickException("No target accounts provided.")
|
|
878
|
+
|
|
879
|
+
self.validate_release_channel(release_channel)
|
|
880
|
+
self._validate_target_accounts(target_accounts)
|
|
881
|
+
|
|
882
|
+
get_snowflake_facade().add_accounts_to_release_channel(
|
|
883
|
+
package_name=self.name,
|
|
884
|
+
release_channel=release_channel,
|
|
885
|
+
target_accounts=target_accounts,
|
|
886
|
+
role=self.role,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
def action_release_channel_remove_accounts(
|
|
890
|
+
self,
|
|
891
|
+
action_ctx: ActionContext,
|
|
892
|
+
release_channel: str,
|
|
893
|
+
target_accounts: list[str],
|
|
894
|
+
*args,
|
|
895
|
+
**kwargs,
|
|
896
|
+
):
|
|
897
|
+
"""
|
|
898
|
+
Removes target accounts from a release channel.
|
|
899
|
+
"""
|
|
900
|
+
|
|
901
|
+
if not target_accounts:
|
|
902
|
+
raise ClickException("No target accounts provided.")
|
|
903
|
+
|
|
904
|
+
self.validate_release_channel(release_channel)
|
|
905
|
+
self._validate_target_accounts(target_accounts)
|
|
906
|
+
|
|
907
|
+
get_snowflake_facade().remove_accounts_from_release_channel(
|
|
908
|
+
package_name=self.name,
|
|
909
|
+
release_channel=release_channel,
|
|
910
|
+
target_accounts=target_accounts,
|
|
911
|
+
role=self.role,
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
def action_release_channel_add_version(
|
|
915
|
+
self,
|
|
916
|
+
action_ctx: ActionContext,
|
|
917
|
+
release_channel: str,
|
|
918
|
+
version: str,
|
|
919
|
+
*args,
|
|
920
|
+
**kwargs,
|
|
921
|
+
):
|
|
922
|
+
"""
|
|
923
|
+
Adds a version to a release channel.
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
self.validate_release_channel(release_channel)
|
|
927
|
+
get_snowflake_facade().add_version_to_release_channel(
|
|
928
|
+
package_name=self.name,
|
|
929
|
+
release_channel=release_channel,
|
|
930
|
+
version=version,
|
|
931
|
+
role=self.role,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
def action_release_channel_remove_version(
|
|
935
|
+
self,
|
|
936
|
+
action_ctx: ActionContext,
|
|
937
|
+
release_channel: str,
|
|
938
|
+
version: str,
|
|
939
|
+
*args,
|
|
940
|
+
**kwargs,
|
|
941
|
+
):
|
|
942
|
+
"""
|
|
943
|
+
Removes a version from a release channel.
|
|
944
|
+
"""
|
|
945
|
+
|
|
946
|
+
self.validate_release_channel(release_channel)
|
|
947
|
+
get_snowflake_facade().remove_version_from_release_channel(
|
|
948
|
+
package_name=self.name,
|
|
949
|
+
release_channel=release_channel,
|
|
950
|
+
version=version,
|
|
951
|
+
role=self.role,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
def _find_version_with_no_recent_update(
|
|
955
|
+
self, versions_info: list[Version], free_versions: set[str]
|
|
956
|
+
) -> Optional[str]:
|
|
957
|
+
"""
|
|
958
|
+
Finds the version with the oldest created_on date from the free versions.
|
|
959
|
+
"""
|
|
960
|
+
|
|
961
|
+
if not free_versions:
|
|
962
|
+
return None
|
|
963
|
+
|
|
964
|
+
# map of versionId to last Updated Date. Last Updated Date is based on patch creation date.
|
|
965
|
+
last_updated_map: dict[str, datetime] = {}
|
|
966
|
+
for version_info in versions_info:
|
|
967
|
+
last_updated_value = last_updated_map.get(version_info["version"], None)
|
|
968
|
+
if (
|
|
969
|
+
not last_updated_value
|
|
970
|
+
or version_info["created_on"] > last_updated_value
|
|
971
|
+
):
|
|
972
|
+
last_updated_map[version_info["version"]] = version_info["created_on"]
|
|
973
|
+
|
|
974
|
+
oldest_version = None
|
|
975
|
+
oldest_version_last_updated_on = None
|
|
976
|
+
|
|
977
|
+
for version in free_versions:
|
|
978
|
+
last_updated = last_updated_map[version]
|
|
979
|
+
if not oldest_version or last_updated < oldest_version_last_updated_on:
|
|
980
|
+
oldest_version = version
|
|
981
|
+
oldest_version_last_updated_on = last_updated
|
|
982
|
+
|
|
983
|
+
return oldest_version
|
|
984
|
+
|
|
985
|
+
def action_publish(
|
|
986
|
+
self,
|
|
987
|
+
action_ctx: ActionContext,
|
|
988
|
+
version: Optional[str],
|
|
989
|
+
patch: Optional[int],
|
|
990
|
+
release_channel: Optional[str],
|
|
991
|
+
release_directive: str,
|
|
992
|
+
interactive: bool,
|
|
993
|
+
force: bool,
|
|
994
|
+
*args,
|
|
995
|
+
create_version: bool = False,
|
|
996
|
+
from_stage: bool = False,
|
|
997
|
+
label: Optional[str] = None,
|
|
998
|
+
**kwargs,
|
|
999
|
+
) -> VersionInfo:
|
|
1000
|
+
"""
|
|
1001
|
+
Publishes a version and a patch to a release directive of a release channel.
|
|
1002
|
+
|
|
1003
|
+
The version is first added to the release channel,
|
|
1004
|
+
and then the release directive is set to the version and patch provided.
|
|
1005
|
+
|
|
1006
|
+
If the number of versions in a release channel exceeds the maximum allowable versions,
|
|
1007
|
+
the user is prompted to remove an existing version to make space for the new version.
|
|
1008
|
+
"""
|
|
1009
|
+
if force:
|
|
1010
|
+
policy = AllowAlwaysPolicy()
|
|
1011
|
+
elif interactive:
|
|
1012
|
+
policy = AskAlwaysPolicy()
|
|
1013
|
+
else:
|
|
1014
|
+
policy = DenyAlwaysPolicy()
|
|
1015
|
+
|
|
1016
|
+
if from_stage and not create_version:
|
|
1017
|
+
raise UsageError(
|
|
1018
|
+
"--from-stage flag can only be used with --create-version flag."
|
|
1019
|
+
)
|
|
1020
|
+
if label is not None and not create_version:
|
|
1021
|
+
raise UsageError("--label can only be used with --create-version flag.")
|
|
1022
|
+
|
|
1023
|
+
console = self._workspace_ctx.console
|
|
1024
|
+
if create_version:
|
|
1025
|
+
result = self.action_version_create(
|
|
1026
|
+
action_ctx=action_ctx,
|
|
1027
|
+
version=version,
|
|
1028
|
+
patch=patch,
|
|
1029
|
+
label=label,
|
|
1030
|
+
skip_git_check=True,
|
|
1031
|
+
interactive=interactive,
|
|
1032
|
+
force=force,
|
|
1033
|
+
from_stage=from_stage,
|
|
1034
|
+
)
|
|
1035
|
+
version = result.version_name
|
|
1036
|
+
patch = result.patch_number
|
|
1037
|
+
|
|
1038
|
+
if version is None:
|
|
1039
|
+
raise UsageError(
|
|
1040
|
+
"Please provide a version using --version or use --create-version flag to create a version based on the manifest file."
|
|
1041
|
+
)
|
|
1042
|
+
if patch is None:
|
|
1043
|
+
raise UsageError(
|
|
1044
|
+
"Please provide a patch number using --patch or use --create-version flag to auto create a patch."
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
versions_info = get_snowflake_facade().show_versions(self.name, self.role)
|
|
1048
|
+
|
|
1049
|
+
available_patches = [
|
|
1050
|
+
version_info["patch"]
|
|
1051
|
+
for version_info in versions_info
|
|
1052
|
+
if version_info["version"] == unquote_identifier(version)
|
|
1053
|
+
]
|
|
1054
|
+
|
|
1055
|
+
if not available_patches:
|
|
1056
|
+
raise ClickException(
|
|
1057
|
+
f"Version {version} does not exist in application package {self.name}. Use --create-version flag to create a new version."
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
if patch not in available_patches:
|
|
1061
|
+
raise ClickException(
|
|
1062
|
+
f"Patch {patch} does not exist for version {version} in application package {self.name}. Use --create-version flag to add a new patch."
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
available_release_channels = get_snowflake_facade().show_release_channels(
|
|
1066
|
+
self.name, self.role
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
release_channel = self.get_sanitized_release_channel(
|
|
1070
|
+
release_channel, available_release_channels
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
if release_channel:
|
|
1074
|
+
release_channel_info = {}
|
|
1075
|
+
for channel_info in available_release_channels:
|
|
1076
|
+
if channel_info["name"] == unquote_identifier(release_channel):
|
|
1077
|
+
release_channel_info = channel_info
|
|
1078
|
+
break
|
|
1079
|
+
|
|
1080
|
+
versions_in_channel = release_channel_info["versions"]
|
|
1081
|
+
if unquote_identifier(version) not in release_channel_info["versions"]:
|
|
1082
|
+
if len(versions_in_channel) >= MAX_VERSIONS_IN_RELEASE_CHANNEL:
|
|
1083
|
+
# If we hit the maximum allowable versions in a release channel, we need to remove one version to make space for the new version
|
|
1084
|
+
all_release_directives = (
|
|
1085
|
+
get_snowflake_facade().show_release_directives(
|
|
1086
|
+
package_name=self.name,
|
|
1087
|
+
role=self.role,
|
|
1088
|
+
release_channel=release_channel,
|
|
1089
|
+
)
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
# check which versions are attached to any release directive
|
|
1093
|
+
targeted_versions = {d["version"] for d in all_release_directives}
|
|
1094
|
+
|
|
1095
|
+
free_versions = {
|
|
1096
|
+
v for v in versions_in_channel if v not in targeted_versions
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if not free_versions:
|
|
1100
|
+
raise ClickException(
|
|
1101
|
+
f"Maximum number of versions in release channel {release_channel} reached. Cannot add more versions."
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
version_to_remove = self._find_version_with_no_recent_update(
|
|
1105
|
+
versions_info, free_versions
|
|
1106
|
+
)
|
|
1107
|
+
user_prompt = f"Maximum number of versions in release channel reached. Would you like to remove version {version_to_remove} to make space for version {version}?"
|
|
1108
|
+
if not policy.should_proceed(user_prompt):
|
|
1109
|
+
raise ClickException(
|
|
1110
|
+
"Cannot proceed with publishing the new version. Please remove an existing version from the release channel to make space for the new version, or use --force to automatically clean up unused versions."
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
console.warning(
|
|
1114
|
+
f"Maximum number of versions in release channel reached. Removing version {version_to_remove} from release_channel {release_channel} to make space for version {version}."
|
|
1115
|
+
)
|
|
1116
|
+
get_snowflake_facade().remove_version_from_release_channel(
|
|
1117
|
+
package_name=self.name,
|
|
1118
|
+
release_channel=release_channel,
|
|
1119
|
+
version=version_to_remove,
|
|
1120
|
+
role=self.role,
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
get_snowflake_facade().add_version_to_release_channel(
|
|
1124
|
+
package_name=self.name,
|
|
1125
|
+
release_channel=release_channel,
|
|
1126
|
+
version=version,
|
|
1127
|
+
role=self.role,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
get_snowflake_facade().set_release_directive(
|
|
1131
|
+
package_name=self.name,
|
|
1132
|
+
release_directive=release_directive,
|
|
1133
|
+
release_channel=release_channel,
|
|
1134
|
+
target_accounts=None,
|
|
1135
|
+
version=version,
|
|
1136
|
+
patch=patch,
|
|
1137
|
+
role=self.role,
|
|
1138
|
+
)
|
|
1139
|
+
return VersionInfo(version, patch, None)
|
|
1140
|
+
|
|
1141
|
+
def _bundle_children(self, action_ctx: ActionContext) -> List[str]:
|
|
1142
|
+
# Create _children directory
|
|
1143
|
+
children_artifacts_dir = self.children_artifacts_deploy_root
|
|
1144
|
+
os.makedirs(children_artifacts_dir)
|
|
1145
|
+
children_sql = []
|
|
1146
|
+
for child in self._entity_model.children:
|
|
1147
|
+
# Create child sub directory
|
|
1148
|
+
child_artifacts_dir = children_artifacts_dir / sanitize_dir_name(
|
|
1149
|
+
child.target
|
|
1150
|
+
)
|
|
1151
|
+
try:
|
|
1152
|
+
os.makedirs(child_artifacts_dir)
|
|
1153
|
+
except FileExistsError:
|
|
1154
|
+
raise ClickException(
|
|
1155
|
+
f"Could not create sub-directory at {child_artifacts_dir}. Make sure child entity names do not collide with each other."
|
|
1156
|
+
)
|
|
1157
|
+
child_entity: ApplicationPackageChildInterface = action_ctx.get_entity(
|
|
1158
|
+
child.target
|
|
1159
|
+
)
|
|
1160
|
+
child_entity.bundle(child_artifacts_dir)
|
|
1161
|
+
app_role = (
|
|
1162
|
+
to_identifier(
|
|
1163
|
+
child.ensure_usable_by.application_roles.pop() # TODO Support more than one application role
|
|
1164
|
+
)
|
|
1165
|
+
if child.ensure_usable_by and child.ensure_usable_by.application_roles
|
|
1166
|
+
else None
|
|
1167
|
+
)
|
|
1168
|
+
child_schema = (
|
|
1169
|
+
to_identifier(child.identifier.schema_)
|
|
1170
|
+
if child.identifier and child.identifier.schema_
|
|
1171
|
+
else None
|
|
1172
|
+
)
|
|
1173
|
+
children_sql.append(
|
|
1174
|
+
child_entity.get_deploy_sql(
|
|
1175
|
+
artifacts_dir=child_artifacts_dir.relative_to(self.deploy_root),
|
|
1176
|
+
schema=child_schema,
|
|
1177
|
+
# TODO Allow users to override the hard-coded value for specific children
|
|
1178
|
+
replace=True,
|
|
1179
|
+
)
|
|
1180
|
+
)
|
|
1181
|
+
if app_role:
|
|
1182
|
+
children_sql.append(
|
|
1183
|
+
f"CREATE APPLICATION ROLE IF NOT EXISTS {app_role};"
|
|
1184
|
+
)
|
|
1185
|
+
if child_schema:
|
|
1186
|
+
children_sql.append(
|
|
1187
|
+
f"GRANT USAGE ON SCHEMA {child_schema} TO APPLICATION ROLE {app_role};"
|
|
1188
|
+
)
|
|
1189
|
+
children_sql.append(
|
|
1190
|
+
child_entity.get_usage_grant_sql(
|
|
1191
|
+
app_role=app_role, schema=child_schema
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
return children_sql
|
|
1195
|
+
|
|
555
1196
|
def _deploy(
|
|
556
1197
|
self,
|
|
1198
|
+
action_ctx: ActionContext,
|
|
557
1199
|
bundle_map: BundleMap | None,
|
|
558
1200
|
prune: bool,
|
|
559
1201
|
recursive: bool,
|
|
@@ -578,7 +1220,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
578
1220
|
stage_fqn = stage_fqn or self.stage_fqn
|
|
579
1221
|
|
|
580
1222
|
# 1. Create a bundle if one wasn't passed in
|
|
581
|
-
bundle_map = bundle_map or self._bundle()
|
|
1223
|
+
bundle_map = bundle_map or self._bundle(action_ctx)
|
|
582
1224
|
|
|
583
1225
|
# 2. Create an empty application package, if none exists
|
|
584
1226
|
try:
|
|
@@ -610,6 +1252,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
610
1252
|
|
|
611
1253
|
if validate:
|
|
612
1254
|
self.validate_setup_script(
|
|
1255
|
+
action_ctx=action_ctx,
|
|
613
1256
|
use_scratch_stage=False,
|
|
614
1257
|
interactive=interactive,
|
|
615
1258
|
force=force,
|
|
@@ -657,7 +1300,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
657
1300
|
It executes a 'show release directives in application package' query and returns the filtered results, if they exist.
|
|
658
1301
|
"""
|
|
659
1302
|
release_directives = get_snowflake_facade().show_release_directives(
|
|
660
|
-
self.name, self.role
|
|
1303
|
+
package_name=self.name, role=self.role
|
|
661
1304
|
)
|
|
662
1305
|
return [
|
|
663
1306
|
directive
|
|
@@ -688,9 +1331,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
688
1331
|
|
|
689
1332
|
def add_new_patch_to_version(
|
|
690
1333
|
self, version: str, patch: int | None = None, label: str | None = None
|
|
691
|
-
):
|
|
1334
|
+
) -> int:
|
|
692
1335
|
"""
|
|
693
1336
|
Add a new patch, optionally a custom one, to an existing version in an application package.
|
|
1337
|
+
Returns the patch number of the newly created patch.
|
|
694
1338
|
"""
|
|
695
1339
|
console = self._workspace_ctx.console
|
|
696
1340
|
|
|
@@ -710,6 +1354,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
710
1354
|
console.message(
|
|
711
1355
|
f"Patch {new_patch}{with_label_prompt} created for version {version} defined in application package {self.name}."
|
|
712
1356
|
)
|
|
1357
|
+
return new_patch
|
|
713
1358
|
|
|
714
1359
|
def check_index_changes_in_git_repo(
|
|
715
1360
|
self, policy: PolicyBase, interactive: bool
|
|
@@ -818,6 +1463,28 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
818
1463
|
return False
|
|
819
1464
|
return True
|
|
820
1465
|
|
|
1466
|
+
def _get_enable_release_channels_flag(self) -> Optional[bool]:
|
|
1467
|
+
"""
|
|
1468
|
+
Returns the requested value of enable_release_channels flag for the application package.
|
|
1469
|
+
It retrieves the value from the configuration file and checks that the feature is enabled in the account.
|
|
1470
|
+
If return value is None, it means do not explicitly set the flag.
|
|
1471
|
+
"""
|
|
1472
|
+
feature_flag_from_config = FeatureFlag.ENABLE_RELEASE_CHANNELS.get_value()
|
|
1473
|
+
feature_enabled_in_account = (
|
|
1474
|
+
get_snowflake_facade().get_ui_parameter(
|
|
1475
|
+
UIParameter.NA_FEATURE_RELEASE_CHANNELS, "ENABLED"
|
|
1476
|
+
)
|
|
1477
|
+
== "ENABLED"
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
if feature_flag_from_config is not None and not feature_enabled_in_account:
|
|
1481
|
+
self._workspace_ctx.console.warning(
|
|
1482
|
+
f"Ignoring feature flag {FeatureFlag.ENABLE_RELEASE_CHANNELS.name} because release channels are not enabled in the current account."
|
|
1483
|
+
)
|
|
1484
|
+
return None
|
|
1485
|
+
|
|
1486
|
+
return feature_flag_from_config
|
|
1487
|
+
|
|
821
1488
|
def create_app_package(self) -> None:
|
|
822
1489
|
"""
|
|
823
1490
|
Creates the application package with our up-to-date stage if none exists.
|
|
@@ -845,21 +1512,23 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
845
1512
|
if row_comment not in ALLOWED_SPECIAL_COMMENTS:
|
|
846
1513
|
raise ApplicationPackageAlreadyExistsError(self.name)
|
|
847
1514
|
|
|
1515
|
+
# 4. Update the application package with setting enable_release_channels if necessary
|
|
1516
|
+
get_snowflake_facade().alter_application_package_properties(
|
|
1517
|
+
package_name=self.name,
|
|
1518
|
+
enable_release_channels=self._get_enable_release_channels_flag(),
|
|
1519
|
+
role=self.role,
|
|
1520
|
+
)
|
|
1521
|
+
|
|
848
1522
|
return
|
|
849
1523
|
|
|
850
1524
|
# If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
comment = {SPECIAL_COMMENT}
|
|
859
|
-
distribution = {model.distribution}
|
|
860
|
-
"""
|
|
861
|
-
)
|
|
862
|
-
)
|
|
1525
|
+
console.step(f"Creating new application package {self.name} in account.")
|
|
1526
|
+
get_snowflake_facade().create_application_package(
|
|
1527
|
+
role=self.role,
|
|
1528
|
+
enable_release_channels=self._get_enable_release_channels_flag(),
|
|
1529
|
+
distribution=model.distribution,
|
|
1530
|
+
package_name=self.name,
|
|
1531
|
+
)
|
|
863
1532
|
|
|
864
1533
|
def execute_post_deploy_hooks(self):
|
|
865
1534
|
execute_post_deploy_hooks(
|
|
@@ -873,7 +1542,11 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
873
1542
|
)
|
|
874
1543
|
|
|
875
1544
|
def validate_setup_script(
|
|
876
|
-
self,
|
|
1545
|
+
self,
|
|
1546
|
+
action_ctx: ActionContext,
|
|
1547
|
+
use_scratch_stage: bool,
|
|
1548
|
+
interactive: bool,
|
|
1549
|
+
force: bool,
|
|
877
1550
|
):
|
|
878
1551
|
workspace_ctx = self._workspace_ctx
|
|
879
1552
|
console = workspace_ctx.console
|
|
@@ -881,6 +1554,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
881
1554
|
"""Validates Native App setup script SQL."""
|
|
882
1555
|
with console.phase(f"Validating Snowflake Native App setup script."):
|
|
883
1556
|
validation_result = self.get_validation_result(
|
|
1557
|
+
action_ctx=action_ctx,
|
|
884
1558
|
use_scratch_stage=use_scratch_stage,
|
|
885
1559
|
force=force,
|
|
886
1560
|
interactive=interactive,
|
|
@@ -900,14 +1574,20 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
900
1574
|
if validation_result["status"] == "FAIL":
|
|
901
1575
|
raise SetupScriptFailedValidation()
|
|
902
1576
|
|
|
1577
|
+
@span("validate_setup_script")
|
|
903
1578
|
def get_validation_result(
|
|
904
|
-
self,
|
|
1579
|
+
self,
|
|
1580
|
+
action_ctx: ActionContext,
|
|
1581
|
+
use_scratch_stage: bool,
|
|
1582
|
+
interactive: bool,
|
|
1583
|
+
force: bool,
|
|
905
1584
|
):
|
|
906
1585
|
"""Call system$validate_native_app_setup() to validate deployed Native App setup script."""
|
|
907
1586
|
stage_fqn = self.stage_fqn
|
|
908
1587
|
if use_scratch_stage:
|
|
909
1588
|
stage_fqn = self.scratch_stage_fqn
|
|
910
1589
|
self._deploy(
|
|
1590
|
+
action_ctx=action_ctx,
|
|
911
1591
|
bundle_map=None,
|
|
912
1592
|
prune=True,
|
|
913
1593
|
recursive=True,
|
|
@@ -951,7 +1631,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
951
1631
|
bundle_map: BundleMap | None,
|
|
952
1632
|
policy: PolicyBase,
|
|
953
1633
|
interactive: bool,
|
|
954
|
-
):
|
|
1634
|
+
) -> VersionInfo:
|
|
955
1635
|
"""Determine version name, patch number, and label from CLI provided values and manifest.yml version entry.
|
|
956
1636
|
@param [Optional] version: version name as specified in the command
|
|
957
1637
|
@param [Optional] patch: patch number as specified in the command
|
|
@@ -959,12 +1639,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
959
1639
|
@param [Optional] bundle_map: bundle_map if a deploy_root is prepared. _bundle() is performed otherwise.
|
|
960
1640
|
@param policy: CLI policy
|
|
961
1641
|
@param interactive: True if command is run in interactive mode, otherwise False
|
|
1642
|
+
|
|
1643
|
+
@return VersionInfo: version_name, patch_number, label resolved from CLI and manifest.yml
|
|
962
1644
|
"""
|
|
963
1645
|
console = self._workspace_ctx.console
|
|
964
1646
|
|
|
965
1647
|
resolved_version = None
|
|
966
1648
|
resolved_patch = None
|
|
967
|
-
resolved_label =
|
|
1649
|
+
resolved_label = None
|
|
968
1650
|
|
|
969
1651
|
# If version is specified in CLI, no version information from manifest.yml is used (except for comment, we can't control comment as of now).
|
|
970
1652
|
if version is not None:
|
|
@@ -972,7 +1654,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
972
1654
|
"Ignoring version information from the application manifest since a version was explicitly specified with the command."
|
|
973
1655
|
)
|
|
974
1656
|
resolved_patch = patch
|
|
975
|
-
resolved_label = label
|
|
1657
|
+
resolved_label = label
|
|
976
1658
|
resolved_version = version
|
|
977
1659
|
|
|
978
1660
|
# When version is not set by CLI, version name is read from manifest.yml. patch and label from CLI will be used, if provided.
|
|
@@ -1026,17 +1708,18 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
1026
1708
|
resolved_label = label if label is not None else label_manifest
|
|
1027
1709
|
|
|
1028
1710
|
# Check if patch needs to throw a bad option error, either if application package does not exist or if version does not exist
|
|
1029
|
-
|
|
1711
|
+
# If patch is 0 and version does not exist, it is a valid case, because patch 0 is the first patch in a version.
|
|
1712
|
+
if resolved_patch:
|
|
1030
1713
|
try:
|
|
1031
|
-
if not self.get_existing_version_info(resolved_version):
|
|
1714
|
+
if not self.get_existing_version_info(to_identifier(resolved_version)):
|
|
1032
1715
|
raise BadOptionUsage(
|
|
1033
1716
|
option_name="patch",
|
|
1034
|
-
message=f"
|
|
1717
|
+
message=f"Version {resolved_version} is not defined in the application package {self.name}. Try again with a patch of 0 or without specifying any patch.",
|
|
1035
1718
|
)
|
|
1036
1719
|
except ApplicationPackageDoesNotExistError as app_err:
|
|
1037
1720
|
raise BadOptionUsage(
|
|
1038
1721
|
option_name="patch",
|
|
1039
|
-
message=f"
|
|
1722
|
+
message=f"Application package {self.name} does not exist yet. Try again with a patch of 0 or without specifying any patch.",
|
|
1040
1723
|
)
|
|
1041
1724
|
|
|
1042
1725
|
return VersionInfo(
|