snowflake-cli 3.4.1__py3-none-any.whl → 3.6.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 +13 -1
- snowflake/cli/_app/cli_app.py +1 -10
- snowflake/cli/_app/commands_registration/builtin_plugins.py +7 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +3 -3
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_app/snow_connector.py +5 -4
- snowflake/cli/_app/telemetry.py +3 -15
- snowflake/cli/_app/version_check.py +4 -4
- snowflake/cli/_plugins/auth/__init__.py +11 -0
- snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
- snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
- snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
- snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +79 -5
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
- snowflake/cli/_plugins/notebook/commands.py +3 -4
- snowflake/cli/_plugins/object/command_aliases.py +3 -1
- snowflake/cli/_plugins/object/manager.py +4 -2
- snowflake/cli/_plugins/plugin/commands.py +79 -0
- snowflake/cli/_plugins/plugin/manager.py +74 -0
- snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/__init__.py +0 -0
- snowflake/cli/_plugins/project/commands.py +173 -0
- snowflake/cli/{_app/api_impl/plugin/__init__.py → _plugins/project/feature_flags.py} +9 -0
- snowflake/cli/_plugins/project/manager.py +76 -0
- snowflake/cli/_plugins/project/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/project_entity_model.py +40 -0
- snowflake/cli/_plugins/snowpark/commands.py +2 -1
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +70 -10
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
- snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +53 -0
- snowflake/cli/_plugins/spcs/services/manager.py +114 -0
- snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
- snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
- snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
- snowflake/cli/_plugins/sql/manager.py +42 -51
- snowflake/cli/_plugins/sql/source_reader.py +230 -0
- snowflake/cli/_plugins/stage/manager.py +10 -4
- snowflake/cli/_plugins/streamlit/commands.py +9 -24
- snowflake/cli/_plugins/streamlit/manager.py +5 -36
- snowflake/cli/api/artifacts/upload.py +51 -0
- snowflake/cli/api/commands/flags.py +35 -10
- snowflake/cli/api/commands/snow_typer.py +12 -0
- snowflake/cli/api/commands/utils.py +2 -0
- snowflake/cli/api/config.py +15 -10
- snowflake/cli/api/constants.py +2 -0
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/exceptions.py +15 -1
- snowflake/cli/api/feature_flags.py +2 -0
- snowflake/cli/api/plugins/plugin_config.py +43 -4
- snowflake/cli/api/project/definition_helper.py +31 -0
- snowflake/cli/api/project/schemas/entities/entities.py +26 -0
- snowflake/cli/api/rest_api.py +2 -3
- snowflake/cli/{_app → api}/secret.py +4 -1
- snowflake/cli/api/secure_path.py +16 -4
- snowflake/cli/api/sql_execution.py +7 -3
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +12 -12
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +71 -50
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/api/__init__.py +0 -48
- /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from click import ClickException
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
7
|
+
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
8
|
+
from snowflake.cli.api import exceptions
|
|
9
|
+
from snowflake.cli.api.cli_global_context import (
|
|
10
|
+
_CliGlobalContextAccess,
|
|
11
|
+
get_cli_context,
|
|
12
|
+
)
|
|
13
|
+
from snowflake.cli.api.config import (
|
|
14
|
+
connection_exists,
|
|
15
|
+
get_connection_dict,
|
|
16
|
+
set_config_value,
|
|
17
|
+
)
|
|
18
|
+
from snowflake.cli.api.console import cli_console
|
|
19
|
+
from snowflake.cli.api.constants import ObjectType
|
|
20
|
+
from snowflake.cli.api.identifiers import FQN
|
|
21
|
+
from snowflake.cli.api.secret import SecretType
|
|
22
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
23
|
+
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
24
|
+
from snowflake.connector import DictCursor
|
|
25
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PublicKeyProperty(Enum):
|
|
29
|
+
RSA_PUBLIC_KEY = "RSA_PUBLIC_KEY"
|
|
30
|
+
RSA_PUBLIC_KEY_2 = "RSA_PUBLIC_KEY_2"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthManager(SqlExecutionMixin):
|
|
34
|
+
def setup(
|
|
35
|
+
self,
|
|
36
|
+
connection_name: Optional[str],
|
|
37
|
+
key_length: int,
|
|
38
|
+
output_path: SecurePath,
|
|
39
|
+
private_key_passphrase: SecretType,
|
|
40
|
+
):
|
|
41
|
+
# When the user provide new connection name
|
|
42
|
+
if connection_name and connection_exists(connection_name):
|
|
43
|
+
raise ClickException(
|
|
44
|
+
f"Connection with name {connection_name} already exists."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
cli_context = get_cli_context()
|
|
48
|
+
# When the use not provide connection name, so we overwrite the current connection
|
|
49
|
+
if not connection_name:
|
|
50
|
+
connection_name = cli_context.connection_context.connection_name
|
|
51
|
+
|
|
52
|
+
key_name = AuthManager._get_free_key_name(output_path, connection_name) # type: ignore[arg-type]
|
|
53
|
+
self._generate_key_pair_and_set_public_key(
|
|
54
|
+
user=cli_context.connection.user,
|
|
55
|
+
key_length=key_length,
|
|
56
|
+
output_path=output_path,
|
|
57
|
+
key_name=key_name, # type: ignore[arg-type]
|
|
58
|
+
private_key_passphrase=private_key_passphrase,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._create_or_update_connection(
|
|
62
|
+
current_connection=cli_context.connection_context.connection_name,
|
|
63
|
+
connection_name=connection_name, # type: ignore[arg-type]
|
|
64
|
+
private_key_path=self._get_private_key_path(
|
|
65
|
+
output_path=output_path, key_name=key_name # type: ignore[arg-type]
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def rotate(
|
|
70
|
+
self,
|
|
71
|
+
key_length: int,
|
|
72
|
+
output_path: SecurePath,
|
|
73
|
+
private_key_passphrase: SecretType,
|
|
74
|
+
):
|
|
75
|
+
cli_context = get_cli_context()
|
|
76
|
+
connection_name = cli_context.connection_context.connection_name
|
|
77
|
+
|
|
78
|
+
self._ensure_connection_has_private_key(
|
|
79
|
+
cli_context.connection_context.connection_name
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
public_key, public_key_2 = self._get_public_keys()
|
|
83
|
+
|
|
84
|
+
if not public_key and not public_key_2:
|
|
85
|
+
raise ClickException("No public key found. Use the setup command first.")
|
|
86
|
+
|
|
87
|
+
if public_key_2:
|
|
88
|
+
self.set_public_key(
|
|
89
|
+
cli_context.connection.user,
|
|
90
|
+
PublicKeyProperty.RSA_PUBLIC_KEY,
|
|
91
|
+
public_key_2,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
key_name = AuthManager._get_free_key_name(output_path, connection_name)
|
|
95
|
+
public_key = self._generate_keys_and_return_public_key(
|
|
96
|
+
key_length=key_length,
|
|
97
|
+
output_path=output_path,
|
|
98
|
+
key_name=key_name,
|
|
99
|
+
private_key_passphrase=private_key_passphrase,
|
|
100
|
+
)
|
|
101
|
+
self.set_public_key(
|
|
102
|
+
cli_context.connection.user, PublicKeyProperty.RSA_PUBLIC_KEY_2, public_key
|
|
103
|
+
)
|
|
104
|
+
self._create_or_update_connection(
|
|
105
|
+
current_connection=cli_context.connection_context.connection_name,
|
|
106
|
+
connection_name=connection_name,
|
|
107
|
+
private_key_path=self._get_private_key_path(
|
|
108
|
+
output_path=output_path, key_name=key_name
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _generate_key_pair_and_set_public_key(
|
|
113
|
+
self,
|
|
114
|
+
user: str,
|
|
115
|
+
key_length: int,
|
|
116
|
+
output_path: SecurePath,
|
|
117
|
+
key_name: str,
|
|
118
|
+
private_key_passphrase: SecretType,
|
|
119
|
+
):
|
|
120
|
+
public_key_exists, public_key_2_exists = self._get_public_keys()
|
|
121
|
+
|
|
122
|
+
if public_key_exists or public_key_2_exists:
|
|
123
|
+
raise exceptions.CouldNotSetKeyPairError()
|
|
124
|
+
|
|
125
|
+
if not output_path.exists():
|
|
126
|
+
output_path.mkdir(parents=True)
|
|
127
|
+
|
|
128
|
+
public_key = self._generate_keys_and_return_public_key(
|
|
129
|
+
key_length=key_length,
|
|
130
|
+
output_path=output_path,
|
|
131
|
+
key_name=key_name, # type: ignore[arg-type]
|
|
132
|
+
private_key_passphrase=private_key_passphrase,
|
|
133
|
+
)
|
|
134
|
+
self.set_public_key(user, PublicKeyProperty.RSA_PUBLIC_KEY, public_key)
|
|
135
|
+
|
|
136
|
+
def list_keys(self) -> List[Dict]:
|
|
137
|
+
key_properties = [
|
|
138
|
+
"RSA_PUBLIC_KEY",
|
|
139
|
+
"RSA_PUBLIC_KEY_FP",
|
|
140
|
+
"RSA_PUBLIC_KEY_LAST_SET_TIME",
|
|
141
|
+
"RSA_PUBLIC_KEY_2",
|
|
142
|
+
"RSA_PUBLIC_KEY_2_FP",
|
|
143
|
+
"RSA_PUBLIC_KEY_2_LAST_SET_TIME",
|
|
144
|
+
]
|
|
145
|
+
cursor = ObjectManager(connection=self._conn).describe(
|
|
146
|
+
object_type=ObjectType.USER.value.sf_name,
|
|
147
|
+
fqn=FQN.from_string(self._conn.user),
|
|
148
|
+
cursor_class=DictCursor,
|
|
149
|
+
)
|
|
150
|
+
only_public_key_properties = [
|
|
151
|
+
p for p in cursor.fetchall() if p.get("property") in key_properties
|
|
152
|
+
]
|
|
153
|
+
return only_public_key_properties
|
|
154
|
+
|
|
155
|
+
def set_public_key(
|
|
156
|
+
self, user: str, public_key_property: PublicKeyProperty, public_key: str
|
|
157
|
+
) -> SnowflakeCursor:
|
|
158
|
+
return self.execute_query(
|
|
159
|
+
f"ALTER USER {user} SET {public_key_property.value}='{public_key}'"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def remove_public_key(
|
|
163
|
+
self, public_key_property: PublicKeyProperty
|
|
164
|
+
) -> SnowflakeCursor:
|
|
165
|
+
cli_context = get_cli_context()
|
|
166
|
+
return self.execute_query(
|
|
167
|
+
f"ALTER USER {cli_context.connection.user} UNSET {public_key_property.value}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def status(self):
|
|
171
|
+
cli_context = get_cli_context()
|
|
172
|
+
self._ensure_connection_has_private_key(
|
|
173
|
+
cli_context.connection_context.connection_name
|
|
174
|
+
)
|
|
175
|
+
cli_console.step("Private key set for connection - OK")
|
|
176
|
+
self._check_connection(cli_context)
|
|
177
|
+
cli_console.step("Test connection - OK")
|
|
178
|
+
|
|
179
|
+
def extend_connection_add(
|
|
180
|
+
self,
|
|
181
|
+
connection_name: str,
|
|
182
|
+
connection_options: Dict,
|
|
183
|
+
key_length: int,
|
|
184
|
+
output_path: SecurePath,
|
|
185
|
+
private_key_passphrase: SecretType,
|
|
186
|
+
) -> Dict:
|
|
187
|
+
key_name = AuthManager._get_free_key_name(output_path, connection_name)
|
|
188
|
+
|
|
189
|
+
self._generate_key_pair_and_set_public_key(
|
|
190
|
+
user=connection_options["user"],
|
|
191
|
+
key_length=key_length,
|
|
192
|
+
output_path=output_path,
|
|
193
|
+
key_name=key_name,
|
|
194
|
+
private_key_passphrase=private_key_passphrase,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
connection_options["authenticator"] = "SNOWFLAKE_JWT"
|
|
198
|
+
connection_options["private_key_file"] = str(
|
|
199
|
+
self._get_private_key_path(output_path=output_path, key_name=key_name).path
|
|
200
|
+
)
|
|
201
|
+
if connection_options.get("password"):
|
|
202
|
+
del connection_options["password"]
|
|
203
|
+
|
|
204
|
+
return connection_options
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _ensure_connection_has_private_key(connection_name: str) -> None:
|
|
208
|
+
connection = get_connection_dict(connection_name)
|
|
209
|
+
if not connection.get("private_key_file") and not connection.get(
|
|
210
|
+
"private_key_path"
|
|
211
|
+
):
|
|
212
|
+
raise ClickException(
|
|
213
|
+
f"The private key is not set in {connection_name} connection."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _check_connection(cli_context: _CliGlobalContextAccess) -> None:
|
|
218
|
+
cli_context.connection
|
|
219
|
+
|
|
220
|
+
def _get_public_keys(self) -> Tuple[str, str]:
|
|
221
|
+
keys = self.list_keys()
|
|
222
|
+
public_key = ""
|
|
223
|
+
public_key_2 = ""
|
|
224
|
+
for p in keys:
|
|
225
|
+
if (
|
|
226
|
+
p.get("property") == PublicKeyProperty.RSA_PUBLIC_KEY.value
|
|
227
|
+
and p.get("value") != "null"
|
|
228
|
+
):
|
|
229
|
+
public_key = p.get("value") # type: ignore
|
|
230
|
+
if (
|
|
231
|
+
p.get("property") == PublicKeyProperty.RSA_PUBLIC_KEY_2.value
|
|
232
|
+
and p.get("value") != "null"
|
|
233
|
+
):
|
|
234
|
+
public_key_2 = p.get("value") # type: ignore
|
|
235
|
+
return public_key, public_key_2
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _generate_keys_and_return_public_key(
|
|
239
|
+
key_length: int,
|
|
240
|
+
output_path: SecurePath,
|
|
241
|
+
key_name: str,
|
|
242
|
+
private_key_passphrase: SecretType,
|
|
243
|
+
) -> str:
|
|
244
|
+
private_key = rsa.generate_private_key(
|
|
245
|
+
public_exponent=65537,
|
|
246
|
+
key_size=key_length,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if private_key_passphrase:
|
|
250
|
+
pem = SecretType(
|
|
251
|
+
private_key.private_bytes(
|
|
252
|
+
encoding=serialization.Encoding.PEM,
|
|
253
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
254
|
+
encryption_algorithm=serialization.BestAvailableEncryption(
|
|
255
|
+
private_key_passphrase.value.encode("utf-8")
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
cli_console.message(
|
|
260
|
+
"Set the `PRIVATE_KEY_PASSPHRASE` environment variable before using the connection."
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
pem = SecretType(
|
|
264
|
+
private_key.private_bytes(
|
|
265
|
+
encoding=serialization.Encoding.PEM,
|
|
266
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
267
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
with AuthManager._get_private_key_path(output_path, key_name).open(
|
|
272
|
+
mode="wb"
|
|
273
|
+
) as file:
|
|
274
|
+
file.write(pem.value)
|
|
275
|
+
|
|
276
|
+
public_key = private_key.public_key()
|
|
277
|
+
public_pem = public_key.public_bytes(
|
|
278
|
+
encoding=serialization.Encoding.PEM,
|
|
279
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
280
|
+
)
|
|
281
|
+
with AuthManager._get_public_key_path(output_path, key_name).open(
|
|
282
|
+
mode="wb"
|
|
283
|
+
) as file:
|
|
284
|
+
file.write(public_pem)
|
|
285
|
+
|
|
286
|
+
return public_pem.decode("utf-8")
|
|
287
|
+
|
|
288
|
+
@staticmethod
|
|
289
|
+
def _get_free_key_name(output_path: SecurePath, key_name: str) -> str:
|
|
290
|
+
new_private_key = f"{key_name}.p8"
|
|
291
|
+
new_public_key = f"{key_name}.pub"
|
|
292
|
+
new_key_name = key_name
|
|
293
|
+
counter = 1
|
|
294
|
+
|
|
295
|
+
while (
|
|
296
|
+
(output_path / new_private_key).exists()
|
|
297
|
+
and (output_path / new_public_key).exists()
|
|
298
|
+
and counter <= 100
|
|
299
|
+
):
|
|
300
|
+
new_key_name = f"{key_name}_{counter}"
|
|
301
|
+
new_private_key = f"{new_key_name}.p8"
|
|
302
|
+
new_public_key = f"{new_key_name}.pub"
|
|
303
|
+
counter += 1
|
|
304
|
+
|
|
305
|
+
if counter == 100:
|
|
306
|
+
raise ClickException(
|
|
307
|
+
"Too many key pairs with the same name in the output directory."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return new_key_name
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _get_private_key_path(output_path: SecurePath, key_name: str) -> SecurePath:
|
|
314
|
+
return (output_path / f"{key_name}.p8").resolve()
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _get_public_key_path(output_path: SecurePath, key_name: str) -> SecurePath:
|
|
318
|
+
return (output_path / f"{key_name}.pub").resolve()
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _create_or_update_connection(
|
|
322
|
+
current_connection: Optional[str],
|
|
323
|
+
connection_name: str,
|
|
324
|
+
private_key_path: SecurePath,
|
|
325
|
+
):
|
|
326
|
+
connection = get_connection_dict(current_connection)
|
|
327
|
+
connection.pop("password", None)
|
|
328
|
+
connection["authenticator"] = "SNOWFLAKE_JWT"
|
|
329
|
+
connection["private_key_file"] = str(private_key_path.path)
|
|
330
|
+
|
|
331
|
+
set_config_value(["connections", connection_name], value=connection)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from snowflake.cli._plugins.auth import app
|
|
16
|
+
from snowflake.cli.api.plugins.command import (
|
|
17
|
+
SNOWCLI_ROOT_COMMAND_PATH,
|
|
18
|
+
CommandSpec,
|
|
19
|
+
CommandType,
|
|
20
|
+
plugin_hook_impl,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@plugin_hook_impl
|
|
25
|
+
def command_spec():
|
|
26
|
+
return CommandSpec(
|
|
27
|
+
parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
|
|
28
|
+
command_type=CommandType.COMMAND_GROUP,
|
|
29
|
+
typer_instance=app.create_instance(),
|
|
30
|
+
)
|
|
@@ -16,8 +16,9 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
import os.path
|
|
19
|
+
from copy import deepcopy
|
|
19
20
|
from pathlib import Path
|
|
20
|
-
from typing import Optional
|
|
21
|
+
from typing import Dict, Optional, Tuple
|
|
21
22
|
|
|
22
23
|
import typer
|
|
23
24
|
from click import ( # type: ignore
|
|
@@ -28,10 +29,14 @@ from click import ( # type: ignore
|
|
|
28
29
|
)
|
|
29
30
|
from click.core import ParameterSource # type: ignore
|
|
30
31
|
from snowflake import connector
|
|
32
|
+
from snowflake.cli._app.snow_connector import connect_to_snowflake
|
|
33
|
+
from snowflake.cli._plugins.auth.keypair.commands import KEY_PAIR_DEFAULT_PATH
|
|
34
|
+
from snowflake.cli._plugins.auth.keypair.manager import AuthManager
|
|
31
35
|
from snowflake.cli._plugins.connection.util import (
|
|
32
36
|
strip_if_value_present,
|
|
33
37
|
)
|
|
34
38
|
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
39
|
+
from snowflake.cli.api import exceptions
|
|
35
40
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
36
41
|
from snowflake.cli.api.commands.flags import (
|
|
37
42
|
PLAIN_PASSWORD_MSG,
|
|
@@ -67,6 +72,8 @@ from snowflake.cli.api.output.types import (
|
|
|
67
72
|
MessageResult,
|
|
68
73
|
ObjectResult,
|
|
69
74
|
)
|
|
75
|
+
from snowflake.cli.api.secret import SecretType
|
|
76
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
70
77
|
from snowflake.connector import ProgrammingError
|
|
71
78
|
from snowflake.connector.constants import CONNECTIONS_FILE
|
|
72
79
|
|
|
@@ -282,15 +289,26 @@ def add(
|
|
|
282
289
|
if connection_exists(connection_name):
|
|
283
290
|
raise UsageError(f"Connection {connection_name} already exists")
|
|
284
291
|
|
|
292
|
+
if not no_interactive:
|
|
293
|
+
connection_options, keypair_error = _extend_add_with_key_pair(
|
|
294
|
+
connection_name, connection_options
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
keypair_error = ""
|
|
298
|
+
|
|
285
299
|
connections_file = add_connection_to_proper_file(
|
|
286
300
|
connection_name,
|
|
287
301
|
ConnectionConfig(**connection_options),
|
|
288
302
|
)
|
|
289
303
|
if set_as_default:
|
|
290
|
-
set_config_value(
|
|
291
|
-
section=None, key="default_connection_name", value=connection_name
|
|
292
|
-
)
|
|
304
|
+
set_config_value(path=["default_connection_name"], value=connection_name)
|
|
293
305
|
|
|
306
|
+
if keypair_error:
|
|
307
|
+
return MessageResult(
|
|
308
|
+
f"Wrote new password-based connection {connection_name} to {connections_file}, "
|
|
309
|
+
f"however there were some issues during key pair setup. Review the following error and check 'snow auth keypair' "
|
|
310
|
+
f"commands to setup key pair authentication:\n * {keypair_error}"
|
|
311
|
+
)
|
|
294
312
|
return MessageResult(
|
|
295
313
|
f"Wrote new connection {connection_name} to {connections_file}"
|
|
296
314
|
)
|
|
@@ -357,7 +375,7 @@ def set_default(
|
|
|
357
375
|
):
|
|
358
376
|
"""Changes default connection to provided value."""
|
|
359
377
|
get_connection_dict(connection_name=name)
|
|
360
|
-
set_config_value(
|
|
378
|
+
set_config_value(path=["default_connection_name"], value=name)
|
|
361
379
|
return MessageResult(f"Default connection set to: {name}")
|
|
362
380
|
|
|
363
381
|
|
|
@@ -404,3 +422,59 @@ def generate_jwt(
|
|
|
404
422
|
return MessageResult(token)
|
|
405
423
|
except (ValueError, TypeError) as err:
|
|
406
424
|
raise ClickException(str(err))
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _extend_add_with_key_pair(
|
|
428
|
+
connection_name: str, connection_options: Dict
|
|
429
|
+
) -> Tuple[Dict, str]:
|
|
430
|
+
if not _should_extend_with_key_pair(connection_options):
|
|
431
|
+
return connection_options, ""
|
|
432
|
+
|
|
433
|
+
configure_key_pair = typer.confirm(
|
|
434
|
+
"Do you want to configure key pair authentication?",
|
|
435
|
+
default=False,
|
|
436
|
+
)
|
|
437
|
+
if not configure_key_pair:
|
|
438
|
+
return connection_options, ""
|
|
439
|
+
|
|
440
|
+
key_length = typer.prompt(
|
|
441
|
+
"Key length",
|
|
442
|
+
default=2048,
|
|
443
|
+
show_default=True,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
output_path = typer.prompt(
|
|
447
|
+
"Output path",
|
|
448
|
+
default=KEY_PAIR_DEFAULT_PATH,
|
|
449
|
+
show_default=True,
|
|
450
|
+
value_proc=lambda value: SecurePath(value),
|
|
451
|
+
)
|
|
452
|
+
private_key_passphrase = typer.prompt(
|
|
453
|
+
"Private key passphrase",
|
|
454
|
+
default="",
|
|
455
|
+
hide_input=True,
|
|
456
|
+
show_default=False,
|
|
457
|
+
value_proc=lambda value: SecretType(value),
|
|
458
|
+
)
|
|
459
|
+
connection = connect_to_snowflake(temporary_connection=True, **connection_options)
|
|
460
|
+
try:
|
|
461
|
+
connection_options = AuthManager(connection=connection).extend_connection_add(
|
|
462
|
+
connection_name=connection_name,
|
|
463
|
+
connection_options=deepcopy(connection_options),
|
|
464
|
+
key_length=key_length,
|
|
465
|
+
output_path=output_path,
|
|
466
|
+
private_key_passphrase=private_key_passphrase,
|
|
467
|
+
)
|
|
468
|
+
except exceptions.CouldNotSetKeyPairError:
|
|
469
|
+
return connection_options, "The public key is set already."
|
|
470
|
+
except Exception as e:
|
|
471
|
+
return connection_options, str(e)
|
|
472
|
+
return connection_options, ""
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _should_extend_with_key_pair(connection_options: Dict) -> bool:
|
|
476
|
+
return (
|
|
477
|
+
connection_options.get("password") is not None
|
|
478
|
+
and connection_options.get("private_key_file") is None
|
|
479
|
+
and connection_options.get("private_key_path") is None
|
|
480
|
+
)
|
|
@@ -49,7 +49,7 @@ def v1_to_v2(
|
|
|
49
49
|
accept_templates: bool = typer.Option(
|
|
50
50
|
False, "-t", "--accept-templates", help="Allows the migration of templates."
|
|
51
51
|
),
|
|
52
|
-
migrate_local_yml:
|
|
52
|
+
migrate_local_yml: Optional[bool] = typer.Option(
|
|
53
53
|
None,
|
|
54
54
|
"-l",
|
|
55
55
|
"--migrate-local-overrides/--no-migrate-local-overrides",
|
|
@@ -234,7 +234,7 @@ def _validate_imported_default_connection_name(
|
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
def _convert_connection_from_snowsql_config_section(
|
|
237
|
-
snowsql_connection: list[tuple[str, Any]]
|
|
237
|
+
snowsql_connection: list[tuple[str, Any]],
|
|
238
238
|
) -> dict[str, Any]:
|
|
239
239
|
from ast import literal_eval
|
|
240
240
|
|
|
@@ -290,7 +290,6 @@ def _validate_and_save_connections_imported_from_snowsql(
|
|
|
290
290
|
f"Setting [{default_cli_connection_name}] connection as Snowflake CLI's default connection."
|
|
291
291
|
)
|
|
292
292
|
set_config_value(
|
|
293
|
-
|
|
294
|
-
key="default_connection_name",
|
|
293
|
+
path=["default_connection_name"],
|
|
295
294
|
value=default_cli_connection_name,
|
|
296
295
|
)
|
|
@@ -669,7 +669,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
|
|
|
669
669
|
role_to_use=package.role,
|
|
670
670
|
)
|
|
671
671
|
|
|
672
|
-
|
|
672
|
+
create_app_result, warnings = get_snowflake_facade().create_application(
|
|
673
673
|
name=self.name,
|
|
674
674
|
package_name=package.name,
|
|
675
675
|
install_method=install_method,
|
|
@@ -680,6 +680,9 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
|
|
|
680
680
|
warehouse=self.warehouse,
|
|
681
681
|
release_channel=release_channel,
|
|
682
682
|
)
|
|
683
|
+
for warning in warnings:
|
|
684
|
+
self.console.warning(warning)
|
|
685
|
+
return create_app_result
|
|
683
686
|
|
|
684
687
|
@span("update_app_object")
|
|
685
688
|
def create_or_upgrade_app(
|
|
@@ -60,6 +60,7 @@ from snowflake.cli.api.errno import (
|
|
|
60
60
|
CANNOT_DISABLE_MANDATORY_TELEMETRY,
|
|
61
61
|
CANNOT_DISABLE_RELEASE_CHANNELS,
|
|
62
62
|
CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS,
|
|
63
|
+
CANNOT_SET_DEBUG_MODE_WITH_MANIFEST_VERSION,
|
|
63
64
|
DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
|
|
64
65
|
DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
|
|
65
66
|
INSUFFICIENT_PRIVILEGES,
|
|
@@ -854,7 +855,7 @@ class SnowflakeSQLFacade:
|
|
|
854
855
|
debug_mode: bool | None,
|
|
855
856
|
should_authorize_event_sharing: bool | None,
|
|
856
857
|
release_channel: str | None = None,
|
|
857
|
-
) -> list[tuple[str]]:
|
|
858
|
+
) -> tuple[list[tuple[str]], list[str]]:
|
|
858
859
|
"""
|
|
859
860
|
Creates a new application object using an application package,
|
|
860
861
|
running the setup script of the application package
|
|
@@ -868,6 +869,7 @@ class SnowflakeSQLFacade:
|
|
|
868
869
|
@param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
|
|
869
870
|
@param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled
|
|
870
871
|
@param release_channel [Optional]: Release channel to use when creating the application
|
|
872
|
+
@return: a tuple containing the result of the create application query and possible warning messages
|
|
871
873
|
"""
|
|
872
874
|
package_name = to_identifier(package_name)
|
|
873
875
|
name = to_identifier(name)
|
|
@@ -875,11 +877,9 @@ class SnowflakeSQLFacade:
|
|
|
875
877
|
|
|
876
878
|
# by default, applications are created in debug mode when possible;
|
|
877
879
|
# this can be overridden in the project definition
|
|
878
|
-
|
|
880
|
+
initial_debug_mode = False
|
|
879
881
|
if install_method.is_dev_mode:
|
|
880
882
|
initial_debug_mode = debug_mode if debug_mode is not None else True
|
|
881
|
-
debug_mode_clause = f"debug_mode = {initial_debug_mode}"
|
|
882
|
-
|
|
883
883
|
authorize_telemetry_clause = ""
|
|
884
884
|
if should_authorize_event_sharing is not None:
|
|
885
885
|
self._log.info(
|
|
@@ -903,13 +903,13 @@ class SnowflakeSQLFacade:
|
|
|
903
903
|
from application package {package_name}
|
|
904
904
|
{using_clause}
|
|
905
905
|
{release_channel_clause}
|
|
906
|
-
{debug_mode_clause}
|
|
907
906
|
{authorize_telemetry_clause}
|
|
908
907
|
comment = {SPECIAL_COMMENT}
|
|
909
908
|
"""
|
|
910
909
|
)
|
|
911
910
|
),
|
|
912
911
|
)
|
|
912
|
+
|
|
913
913
|
except Exception as err:
|
|
914
914
|
if isinstance(err, ProgrammingError):
|
|
915
915
|
if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING:
|
|
@@ -927,9 +927,36 @@ class SnowflakeSQLFacade:
|
|
|
927
927
|
f"Failed to create application {name} with the following error message:\n"
|
|
928
928
|
f"{err.msg}"
|
|
929
929
|
) from err
|
|
930
|
+
|
|
930
931
|
handle_unclassified_error(err, f"Failed to create application {name}.")
|
|
931
932
|
|
|
932
|
-
|
|
933
|
+
warnings = []
|
|
934
|
+
try:
|
|
935
|
+
if initial_debug_mode:
|
|
936
|
+
self._sql_executor.execute_query(
|
|
937
|
+
dedent(
|
|
938
|
+
_strip_empty_lines(
|
|
939
|
+
f"""\
|
|
940
|
+
alter application {name}
|
|
941
|
+
set debug_mode = {initial_debug_mode}
|
|
942
|
+
"""
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
except Exception as err:
|
|
947
|
+
if (
|
|
948
|
+
isinstance(err, ProgrammingError)
|
|
949
|
+
and err.errno == CANNOT_SET_DEBUG_MODE_WITH_MANIFEST_VERSION
|
|
950
|
+
):
|
|
951
|
+
warnings.append(
|
|
952
|
+
"Did not apply debug mode to application because the manifest version is set to 2 or higher. Please use session debugging instead."
|
|
953
|
+
)
|
|
954
|
+
else:
|
|
955
|
+
warnings.append(
|
|
956
|
+
f"Failed to set debug mode for application {name}. {str(err)}"
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
return create_cursor.fetchall(), warnings
|
|
933
960
|
|
|
934
961
|
def create_application_package(
|
|
935
962
|
self,
|
|
@@ -21,9 +21,7 @@ from snowflake.cli._plugins.notebook.notebook_entity_model import NotebookEntity
|
|
|
21
21
|
from snowflake.cli._plugins.notebook.types import NotebookStagePath
|
|
22
22
|
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
|
|
23
23
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
24
|
-
from snowflake.cli.api.commands.decorators import
|
|
25
|
-
with_project_definition,
|
|
26
|
-
)
|
|
24
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
27
25
|
from snowflake.cli.api.commands.flags import (
|
|
28
26
|
ReplaceOption,
|
|
29
27
|
entity_argument,
|
|
@@ -107,7 +105,8 @@ def create(
|
|
|
107
105
|
def deploy(
|
|
108
106
|
entity_id: str = entity_argument("notebook"),
|
|
109
107
|
replace: bool = ReplaceOption(
|
|
110
|
-
help="Replace notebook object if it already exists."
|
|
108
|
+
help="Replace notebook object if it already exists. It only uploads new and overwrites existing files, "
|
|
109
|
+
"but does not remove any files already on the stage.",
|
|
111
110
|
),
|
|
112
111
|
**options,
|
|
113
112
|
) -> CommandResult:
|
|
@@ -36,8 +36,10 @@ def add_object_command_aliases(
|
|
|
36
36
|
name_argument: typer.Argument,
|
|
37
37
|
like_option: Optional[typer.Option],
|
|
38
38
|
scope_option: Optional[typer.Option],
|
|
39
|
-
ommit_commands: List[str] =
|
|
39
|
+
ommit_commands: Optional[List[str]] = None,
|
|
40
40
|
):
|
|
41
|
+
if ommit_commands is None:
|
|
42
|
+
ommit_commands = list()
|
|
41
43
|
if "list" not in ommit_commands:
|
|
42
44
|
if not like_option:
|
|
43
45
|
raise ClickException('[like_option] have to be defined for "list" command')
|
|
@@ -58,14 +58,16 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
58
58
|
object_name = _get_object_names(object_type).sf_name
|
|
59
59
|
return self.execute_query(f"drop {object_name} {fqn.sql_identifier}")
|
|
60
60
|
|
|
61
|
-
def describe(self, *, object_type: str, fqn: FQN):
|
|
61
|
+
def describe(self, *, object_type: str, fqn: FQN, **kwargs):
|
|
62
62
|
# Image repository is the only supported object that does not have a DESCRIBE command.
|
|
63
63
|
if object_type == "image-repository":
|
|
64
64
|
raise ClickException(
|
|
65
65
|
f"Describe is currently not supported for object of type image-repository"
|
|
66
66
|
)
|
|
67
67
|
object_name = _get_object_names(object_type).sf_name
|
|
68
|
-
return self.execute_query(
|
|
68
|
+
return self.execute_query(
|
|
69
|
+
f"describe {object_name} {fqn.sql_identifier}", **kwargs
|
|
70
|
+
)
|
|
69
71
|
|
|
70
72
|
def object_exists(self, *, object_type: str, fqn: FQN):
|
|
71
73
|
try:
|