Skip to content

Commit

Permalink
Snow 1181759 git setup (#880)
Browse files Browse the repository at this point in the history
* initial version of setup command

* Quickcheck whether repository already exists

* unit tests

* integration tests

* small fix; update help messages test

* check setup output in integration tests

* review: refactor git/commands::setup

* review fixes

* Update src/snowflake/cli/plugins/git/commands.py

Co-authored-by: Tomasz Urbaszek <[email protected]>

* refactor setup command

* update unit tests

* update integration tests

* multiline string

---------

Co-authored-by: Tomasz Urbaszek <[email protected]>
  • Loading branch information
sfc-gh-pczajka and sfc-gh-turbaszek authored Mar 11, 2024
1 parent 9d3409a commit 71e8619
Show file tree
Hide file tree
Showing 6 changed files with 463 additions and 27 deletions.
27 changes: 27 additions & 0 deletions src/snowflake/cli/api/sql_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ def use_role(self, new_role: str):
if is_different_role:
self._execute_query(f"use role {prev_role}")

def create_password_secret(
self, name: str, username: str, password: str
) -> SnowflakeCursor:
query = dedent(
f"""
create secret {name}
type = password
username = '{username}'
password = '{password}'
"""
)
return self._execute_query(query)

def create_api_integration(
self, name: str, api_provider: str, allowed_prefix: str, secret: Optional[str]
) -> SnowflakeCursor:
query = dedent(
f"""
create api integration {name}
api_provider = {api_provider}
api_allowed_prefixes = ('{allowed_prefix}')
allowed_authentication_secrets = ({secret if secret else ''})
enabled = true
"""
)
return self._execute_query(query)

def _execute_schema_query(self, query: str, name: Optional[str] = None, **kwargs):
"""
Check that a database and schema are provided before executing the query. Useful for operating on schema level objects.
Expand Down
95 changes: 95 additions & 0 deletions src/snowflake/cli/plugins/git/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
from click import ClickException
from snowflake.cli.api.commands.flags import identifier_argument
from snowflake.cli.api.commands.snow_typer import SnowTyper
from snowflake.cli.api.console.console import cli_console
from snowflake.cli.api.constants import ObjectType
from snowflake.cli.api.output.types import CommandResult, QueryResult
from snowflake.cli.api.utils.path_utils import is_stage_path
from snowflake.cli.plugins.git.manager import GitManager
from snowflake.cli.plugins.object.manager import ObjectManager
from snowflake.connector import ProgrammingError

app = SnowTyper(
name="git",
Expand Down Expand Up @@ -38,6 +42,97 @@ def _repo_path_argument_callback(path):
)


def _object_exists(object_type, identifier):
try:
ObjectManager().describe(
object_type=object_type.value.cli_name,
name=identifier,
)
return True
except ProgrammingError:
return False


def _assure_repository_does_not_exist(repository_name: str) -> None:
if _object_exists(ObjectType.GIT_REPOSITORY, repository_name):
raise ClickException(f"Repository '{repository_name}' already exists")


def _validate_origin_url(url: str) -> None:
if not url.startswith("https://"):
raise ClickException("Url address should start with 'https'")


@app.command("setup", requires_connection=True)
def setup(
repository_name: str = RepoNameArgument,
**options,
) -> CommandResult:
"""
Sets up a git repository object.
You will be prompted for:
* url - address of repository to be used for git clone operation
* secret - Snowflake secret containing authentication credentials. Not needed if origin repository does not require
authentication for RO operations (clone, fetch)
* API integration - object allowing Snowflake to interact with git repository.
"""
_assure_repository_does_not_exist(repository_name)
manager = GitManager()

url = typer.prompt("Origin url")
_validate_origin_url(url)

secret = {}
secret_needed = typer.confirm("Use secret for authentication?")
if secret_needed:
secret_name = f"{repository_name}_secret"
secret_name = typer.prompt(
"Secret identifier (will be created if not exists)", default=secret_name
)
secret = {"name": secret_name}
if _object_exists(ObjectType.SECRET, secret_name):
cli_console.step(f"Using existing secret '{secret_name}'")
else:
cli_console.step(f"Secret '{secret_name}' will be created")
secret["username"] = typer.prompt("username")
secret["password"] = typer.prompt("password/token", hide_input=True)

api_integration = f"{repository_name}_api_integration"
api_integration = typer.prompt(
"API integration identifier (will be created if not exists)",
default=api_integration,
)

if "username" in secret:
manager.create_password_secret(**secret)
secret_name = secret["name"]
cli_console.step(f"Secret '{secret_name}' successfully created.")

if not _object_exists(ObjectType.INTEGRATION, api_integration):
manager.create_api_integration(
name=api_integration,
api_provider="git_https_api",
allowed_prefix=url,
secret=secret.get("name"),
)
cli_console.step(f"API integration '{api_integration}' successfully created.")
else:
cli_console.step(f"Using existing API integration '{api_integration}'.")

return QueryResult(
manager.create(
repo_name=repository_name,
url=url,
api_integration=api_integration,
secret=secret.get("name"),
)
)


@app.command(
"list-branches",
requires_connection=True,
Expand Down
16 changes: 16 additions & 0 deletions src/snowflake/cli/plugins/git/manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from textwrap import dedent

from snowflake.cli.plugins.object.stage.manager import StageManager
from snowflake.connector.cursor import SnowflakeCursor

Expand All @@ -18,3 +20,17 @@ def show_files(self, repo_path: str) -> SnowflakeCursor:
def fetch(self, repo_name: str) -> SnowflakeCursor:
query = f"alter git repository {repo_name} fetch"
return self._execute_query(query)

def create(
self, repo_name: str, api_integration: str, url: str, secret: str
) -> SnowflakeCursor:
query = dedent(
f"""
create git repository {repo_name}
api_integration = {api_integration}
origin = '{url}'
"""
)
if secret is not None:
query += f"git_credentials = {secret}\n"
return self._execute_query(query)
73 changes: 73 additions & 0 deletions tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,78 @@
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_help_messages[git.setup]
'''

Usage: default git setup [OPTIONS] REPOSITORY_NAME

Sets up a git repository object.
You will be prompted for:
* url - address of repository to be used for git clone operation
* secret - Snowflake secret containing authentication credentials. Not needed
if origin repository does not require authentication for RO operations (clone,
fetch)
* API integration - object allowing Snowflake to interact with git repository.

╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ * repository_name TEXT Identifier of the git repository. For │
│ example: my_repo │
│ [default: None] │
│ [required] │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --help -h Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Connection configuration ───────────────────────────────────────────────────╮
│ --connection,--environment -c TEXT Name of the connection, as defined │
│ in your `config.toml`. Default: │
│ `default`. │
│ --account,--accountname TEXT Name assigned to your Snowflake │
│ account. Overrides the value │
│ specified for the connection. │
│ --user,--username TEXT Username to connect to Snowflake. │
│ Overrides the value specified for │
│ the connection. │
│ --password TEXT Snowflake password. Overrides the │
│ value specified for the │
│ connection. │
│ --authenticator TEXT Snowflake authenticator. Overrides │
│ the value specified for the │
│ connection. │
│ --private-key-path TEXT Snowflake private key path. │
│ Overrides the value specified for │
│ the connection. │
│ --database,--dbname TEXT Database to use. Overrides the │
│ value specified for the │
│ connection. │
│ --schema,--schemaname TEXT Database schema to use. Overrides │
│ the value specified for the │
│ connection. │
│ --role,--rolename TEXT Role to use. Overrides the value │
│ specified for the connection. │
│ --warehouse TEXT Warehouse to use. Overrides the │
│ value specified for the │
│ connection. │
│ --temporary-connection -x Uses connection defined with │
│ command line parameters, instead │
│ of one defined in config │
│ --mfa-passcode TEXT Token to use for multi-factor │
│ authentication (MFA) │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Global configuration ───────────────────────────────────────────────────────╮
│ --format [TABLE|JSON] Specifies the output format. │
│ [default: TABLE] │
│ --verbose -v Displays log entries for log levels `info` │
│ and higher. │
│ --debug Displays log entries for log levels `debug` │
│ and higher; debug logs contains additional │
│ information. │
│ --silent Turns off intermediate output to console. │
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_help_messages[git]
Expand All @@ -1146,6 +1218,7 @@
│ list-branches List all branches in the repository. │
│ list-files List files from given state of git repository. │
│ list-tags List all tags in the repository. │
│ setup Sets up a git repository object. │
╰──────────────────────────────────────────────────────────────────────────────╯


Expand Down
Loading

0 comments on commit 71e8619

Please sign in to comment.