From 1c2ef97810b3494119d3cae9d3d1106d227e15b8 Mon Sep 17 00:00:00 2001 From: Michel El Nacouzi Date: Thu, 19 Dec 2024 16:35:50 -0500 Subject: [PATCH] Add snow app release-channel add-version and remove-version commands (#1958) --- RELEASE-NOTES.md | 12 +- .../nativeapp/entities/application_package.py | 40 ++++ .../nativeapp/release_channel/commands.py | 72 ++++++ .../cli/_plugins/nativeapp/sf_sql_facade.py | 88 ++++++++ src/snowflake/cli/api/errno.py | 5 +- tests/__snapshots__/test_help_messages.ambr | 210 ++++++++++++++++++ .../test_application_package_entity.py | 176 +++++++++++++++ tests/nativeapp/test_sf_sql_facade.py | 164 ++++++++++++++ tests/nativeapp/utils.py | 6 + 9 files changed, 767 insertions(+), 6 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 03b8dd9d7e..2763cc0a24 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -20,15 +20,17 @@ * Added deprecation message for default Streamlit warehouse ## New additions -* Add Release Directives support by introducing the following commands: +* Add support for Release Directives by introducing the following commands: * `snow app release-directive list` * `snow app release-directive set` * `snow app release-directive unset` -* Add support for release channels feature in native app version creation/drop. * `snow app version create` now returns version, patch, and label in JSON format. -* Add ability to specify release channel when creating application instance from release directive: `snow app run --from-release-directive --channel=` -* Add ability to list release channels through `snow app release-channel list` command -* Add ability to add and remove accounts from release channels through `snow app release-channel add-accounts` and snow app release-channel remove-accounts` commands. +* Add support for release channels: + * Add support for release channels feature in native app version creation/drop. + * Add ability to specify release channel when creating application instance from release directive: `snow app run --from-release-directive --channel=` + * Add ability to list release channels through `snow app release-channel list` command + * Add ability to add and remove accounts from release channels through `snow app release-channel add-accounts` and snow app release-channel remove-accounts` commands. + * Add ability to add/remove versions to/from release channels through `snow app release-channel add-version` and `snow app release-channel remove-version` commands. ## Fixes and improvements * Fixed crashes with older x86_64 Intel CPUs. diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 453d9fe103..1da13ca9d5 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -930,6 +930,46 @@ def action_release_channel_remove_accounts( role=self.role, ) + def action_release_channel_add_version( + self, + action_ctx: ActionContext, + release_channel: str, + version: str, + *args, + **kwargs, + ): + """ + Adds a version to a release channel. + """ + + self.validate_release_channel(release_channel) + get_snowflake_facade().add_version_to_release_channel( + package_name=self.name, + release_channel=release_channel, + version=version, + role=self.role, + ) + + def action_release_channel_remove_version( + self, + action_ctx: ActionContext, + release_channel: str, + version: str, + *args, + **kwargs, + ): + """ + Removes a version from a release channel. + """ + + self.validate_release_channel(release_channel) + get_snowflake_facade().remove_version_from_release_channel( + package_name=self.name, + release_channel=release_channel, + version=version, + role=self.role, + ) + def _bundle_children(self, action_ctx: ActionContext) -> List[str]: # Create _children directory children_artifacts_dir = self.children_artifacts_deploy_root diff --git a/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py b/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py index 54c484c768..503e445b75 100644 --- a/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py @@ -138,3 +138,75 @@ def release_channel_remove_accounts( ) return MessageResult("Successfully removed accounts from the release channel.") + + +@app.command("add-version", requires_connection=True) +@with_project_definition() +@force_project_definition_v2() +def release_channel_add_version( + channel: str = typer.Argument( + show_default=False, + help="The release channel to add a version to.", + ), + version: str = typer.Option( + show_default=False, + help="The version to add to the release channel.", + ), + **options, +) -> CommandResult: + """ + Adds a version to a release channel. + """ + + cli_context = get_cli_context() + ws = WorkspaceManager( + project_definition=cli_context.project_definition, + project_root=cli_context.project_root, + ) + package_id = options["package_entity_id"] + ws.perform_action( + package_id, + EntityActions.RELEASE_CHANNEL_ADD_VERSION, + release_channel=channel, + version=version, + ) + + return MessageResult( + f"Successfully added version {version} to the release channel." + ) + + +@app.command("remove-version", requires_connection=True) +@with_project_definition() +@force_project_definition_v2() +def release_channel_remove_version( + channel: str = typer.Argument( + show_default=False, + help="The release channel to remove a version from.", + ), + version: str = typer.Option( + show_default=False, + help="The version to remove from the release channel.", + ), + **options, +) -> CommandResult: + """ + Removes a version from a release channel. + """ + + cli_context = get_cli_context() + ws = WorkspaceManager( + project_definition=cli_context.project_definition, + project_root=cli_context.project_root, + ) + package_id = options["package_entity_id"] + ws.perform_action( + package_id, + EntityActions.RELEASE_CHANNEL_REMOVE_VERSION, + release_channel=channel, + version=version, + ) + + return MessageResult( + f"Successfully removed version {version} from the release channel." + ) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 914c7b1605..9df618fc97 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -57,12 +57,16 @@ DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, DOES_NOT_EXIST_OR_NOT_AUTHORIZED, INSUFFICIENT_PRIVILEGES, + MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED, NO_WAREHOUSE_SELECTED_IN_SESSION, RELEASE_DIRECTIVE_DOES_NOT_EXIST, RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND, SQL_COMPILATION_ERROR, + VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL, VERSION_DOES_NOT_EXIST, VERSION_NOT_ADDED_TO_RELEASE_CHANNEL, + VERSION_NOT_IN_RELEASE_CHANNEL, + VERSION_REFERENCED_BY_RELEASE_DIRECTIVE, ) from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.metrics import CLICounterField @@ -1302,6 +1306,90 @@ def remove_accounts_from_release_channel( f"Failed to remove accounts from release channel {release_channel} in application package {package_name}.", ) + def add_version_to_release_channel( + self, + package_name: str, + release_channel: str, + version: str, + role: str | None = None, + ): + """ + Adds a version to a release channel. + + @param package_name: Name of the application package + @param release_channel: Name of the release channel + @param version: Version to add to the release channel + @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in. + """ + + package_name = to_identifier(package_name) + release_channel = to_identifier(release_channel) + version = to_identifier(version) + + with self._use_role_optional(role): + try: + self._sql_executor.execute_query( + f"alter application package {package_name} modify release channel {release_channel} add version {version}" + ) + except Exception as err: + if isinstance(err, ProgrammingError): + if err.errno == VERSION_DOES_NOT_EXIST: + raise UserInputError( + f"Version {version} does not exist in application package {package_name}." + ) from err + if err.errno == VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL: + raise UserInputError( + f"Version {version} is already added to release channel {release_channel}." + ) from err + if err.errno == MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED: + raise UserInputError( + f"Maximum number of versions allowed in release channel {release_channel} has been reached." + ) from err + handle_unclassified_error( + err, + f"Failed to add version {version} to release channel {release_channel} in application package {package_name}.", + ) + + def remove_version_from_release_channel( + self, + package_name: str, + release_channel: str, + version: str, + role: str | None = None, + ): + """ + Removes a version from a release channel. + + @param package_name: Name of the application package + @param release_channel: Name of the release channel + @param version: Version to remove from the release channel + @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in. + """ + + package_name = to_identifier(package_name) + release_channel = to_identifier(release_channel) + version = to_identifier(version) + + with self._use_role_optional(role): + try: + self._sql_executor.execute_query( + f"alter application package {package_name} modify release channel {release_channel} drop version {version}" + ) + except Exception as err: + if isinstance(err, ProgrammingError): + if err.errno == VERSION_NOT_IN_RELEASE_CHANNEL: + raise UserInputError( + f"Version {version} is not found in release channel {release_channel}." + ) from err + if err.errno == VERSION_REFERENCED_BY_RELEASE_DIRECTIVE: + raise UserInputError( + f"Cannot remove version {version} from release channel {release_channel} as it is referenced by a release directive." + ) from err + handle_unclassified_error( + err, + f"Failed to remove version {version} from release channel {release_channel} in application package {package_name}.", + ) + def _strip_empty_lines(text: str) -> str: """ diff --git a/src/snowflake/cli/api/errno.py b/src/snowflake/cli/api/errno.py index c13fdef719..624b491160 100644 --- a/src/snowflake/cli/api/errno.py +++ b/src/snowflake/cli/api/errno.py @@ -49,7 +49,7 @@ APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082 APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE = 93083 APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT = 93084 -APPLICATION_PACKAGE_CANNOT_DROP_VERSION_IF_IT_IS_IN_USE = 93088 +VERSION_REFERENCED_BY_RELEASE_DIRECTIVE = 93088 APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148 CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118 APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128 @@ -66,9 +66,12 @@ RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND = 93036 RELEASE_DIRECTIVE_DOES_NOT_EXIST = 93090 VERSION_DOES_NOT_EXIST = 93031 +VERSION_NOT_IN_RELEASE_CHANNEL = 512010 ACCOUNT_DOES_NOT_EXIST = 1999 ACCOUNT_HAS_TOO_MANY_QUALIFIERS = 906 CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS = 512017 +VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL = 512005 +MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED = 512004 ERR_JAVASCRIPT_EXECUTION = 100132 diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index 8583b2599a..9e8d9b808d 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -682,6 +682,109 @@ +------------------------------------------------------------------------------+ + ''' +# --- +# name: test_help_messages[app.release-channel.add-version] + ''' + + Usage: default app release-channel add-version [OPTIONS] CHANNEL + + Adds a version to a release channel. + + +- Arguments ------------------------------------------------------------------+ + | * channel TEXT The release channel to add a version to. | + | [required] | + +------------------------------------------------------------------------------+ + +- Options --------------------------------------------------------------------+ + | * --version TEXT The version to add to the release | + | channel. | + | [required] | + | --package-entity-id TEXT The ID of the package entity on which | + | to operate when definition_version is | + | 2 or higher. | + | --app-entity-id TEXT The ID of the application entity on | + | which to operate when | + | definition_version is 2 or higher. | + | --project -p TEXT Path where Snowflake project resides. | + | Defaults to current working directory. | + | --env TEXT String in format of key=value. | + | Overrides variables from env section | + | used for templates. | + | --help -h Show this message and exit. | + +------------------------------------------------------------------------------+ + +- Connection configuration ---------------------------------------------------+ + | --connection,--environment -c TEXT Name of the connection, as | + | defined in your config.toml | + | file. Default: default. | + | --host TEXT Host address for the | + | connection. Overrides the | + | value specified for the | + | connection. | + | --port INTEGER Port for the connection. | + | Overrides the value | + | specified for the | + | connection. | + | --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-file,--privateā€¦ TEXT Snowflake private key file | + | path. Overrides the value | + | specified for the | + | connection. | + | --token-file-path TEXT Path to file with an OAuth | + | token that should be used | + | when connecting to Snowflake | + | --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) | + | --enable-diag Run Python connector | + | diagnostic test | + | --diag-log-path TEXT Diagnostic report path | + | --diag-allowlist-path TEXT Diagnostic report path to | + | optional allowlist | + +------------------------------------------------------------------------------+ + +- 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 contain additional | + | information. | + | --silent Turns off intermediate output to console. | + +------------------------------------------------------------------------------+ + + ''' # --- # name: test_help_messages[app.release-channel.list] @@ -886,6 +989,109 @@ +------------------------------------------------------------------------------+ + ''' +# --- +# name: test_help_messages[app.release-channel.remove-version] + ''' + + Usage: default app release-channel remove-version [OPTIONS] CHANNEL + + Removes a version from a release channel. + + +- Arguments ------------------------------------------------------------------+ + | * channel TEXT The release channel to remove a version from. | + | [required] | + +------------------------------------------------------------------------------+ + +- Options --------------------------------------------------------------------+ + | * --version TEXT The version to remove from the release | + | channel. | + | [required] | + | --package-entity-id TEXT The ID of the package entity on which | + | to operate when definition_version is | + | 2 or higher. | + | --app-entity-id TEXT The ID of the application entity on | + | which to operate when | + | definition_version is 2 or higher. | + | --project -p TEXT Path where Snowflake project resides. | + | Defaults to current working directory. | + | --env TEXT String in format of key=value. | + | Overrides variables from env section | + | used for templates. | + | --help -h Show this message and exit. | + +------------------------------------------------------------------------------+ + +- Connection configuration ---------------------------------------------------+ + | --connection,--environment -c TEXT Name of the connection, as | + | defined in your config.toml | + | file. Default: default. | + | --host TEXT Host address for the | + | connection. Overrides the | + | value specified for the | + | connection. | + | --port INTEGER Port for the connection. | + | Overrides the value | + | specified for the | + | connection. | + | --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-file,--privateā€¦ TEXT Snowflake private key file | + | path. Overrides the value | + | specified for the | + | connection. | + | --token-file-path TEXT Path to file with an OAuth | + | token that should be used | + | when connecting to Snowflake | + | --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) | + | --enable-diag Run Python connector | + | diagnostic test | + | --diag-log-path TEXT Diagnostic report path | + | --diag-allowlist-path TEXT Diagnostic report path to | + | optional allowlist | + +------------------------------------------------------------------------------+ + +- 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 contain additional | + | information. | + | --silent Turns off intermediate output to console. | + +------------------------------------------------------------------------------+ + + ''' # --- # name: test_help_messages[app.release-channel] @@ -900,9 +1106,11 @@ +------------------------------------------------------------------------------+ +- Commands -------------------------------------------------------------------+ | add-accounts Adds accounts to a release channel. | + | add-version Adds a version to a release channel. | | list Lists the release channels available for an application | | package. | | remove-accounts Removes accounts from a release channel. | + | remove-version Removes a version from a release channel. | +------------------------------------------------------------------------------+ @@ -10465,9 +10673,11 @@ +------------------------------------------------------------------------------+ +- Commands -------------------------------------------------------------------+ | add-accounts Adds accounts to a release channel. | + | add-version Adds a version to a release channel. | | list Lists the release channels available for an application | | package. | | remove-accounts Removes accounts from a release channel. | + | remove-version Removes a version from a release channel. | +------------------------------------------------------------------------------+ diff --git a/tests/nativeapp/test_application_package_entity.py b/tests/nativeapp/test_application_package_entity.py index 1185e7d700..a472da7d56 100644 --- a/tests/nativeapp/test_application_package_entity.py +++ b/tests/nativeapp/test_application_package_entity.py @@ -39,9 +39,11 @@ APPLICATION_PACKAGE_ENTITY_MODULE, SQL_EXECUTOR_EXECUTE, SQL_FACADE_ADD_ACCOUNTS_TO_RELEASE_CHANNEL, + SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL, SQL_FACADE_GET_UI_PARAMETER, SQL_FACADE_MODIFY_RELEASE_DIRECTIVE, SQL_FACADE_REMOVE_ACCOUNTS_FROM_RELEASE_CHANNEL, + SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL, SQL_FACADE_SET_RELEASE_DIRECTIVE, SQL_FACADE_SHOW_RELEASE_CHANNELS, SQL_FACADE_SHOW_RELEASE_DIRECTIVES, @@ -1241,3 +1243,177 @@ def test_given_invalid_account_names_when_remove_accounts_from_release_channel_t ) remove_accounts_from_release_channel.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +def test_given_release_channel_and_version_when_release_channel_add_version_then_success( + add_version_to_release_channel, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "test_channel"}] + + application_package_entity.action_release_channel_add_version( + action_ctx=action_context, + release_channel="test_channel", + version="1.0", + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + + add_version_to_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="1.0", + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +def test_given_release_channels_disabled_when_release_channel_add_version_then_error( + add_version_to_release_channel, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [] + + with pytest.raises(UsageError) as e: + application_package_entity.action_release_channel_add_version( + action_ctx=action_context, + release_channel="invalid_channel", + version="1.0", + ) + + assert ( + str(e.value) + == f"Release channels are not enabled for application package {pkg_model.fqn.name}." + ) + + add_version_to_release_channel.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +def test_given_invalid_release_channel_when_release_channel_add_version_then_error( + add_version_to_release_channel, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "test_channel"}] + + with pytest.raises(UsageError) as e: + application_package_entity.action_release_channel_add_version( + action_ctx=action_context, + release_channel="invalid_channel", + version="1.0", + ) + + assert ( + str(e.value) + == f"Release channel invalid_channel is not available in application package {pkg_model.fqn.name}. Available release channels are: (test_channel)." + ) + + add_version_to_release_channel.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +def test_given_release_channel_and_version_when_release_channel_remove_version_then_success( + remove_version_from_release_channel, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "test_channel"}] + + application_package_entity.action_release_channel_remove_version( + action_ctx=action_context, + release_channel="test_channel", + version="1.0", + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + + remove_version_from_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="1.0", + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +def test_given_release_channels_disabled_when_release_channel_remove_version_then_error( + remove_version_from_release_channel, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [] + + with pytest.raises(UsageError) as e: + application_package_entity.action_release_channel_remove_version( + action_ctx=action_context, + release_channel="invalid_channel", + version="1.0", + ) + + assert ( + str(e.value) + == f"Release channels are not enabled for application package {pkg_model.fqn.name}." + ) + + remove_version_from_release_channel.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +def test_given_invalid_release_channel_when_release_channel_remove_version_then_error( + remove_version_from_release_channel, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "test_channel"}] + + with pytest.raises(UsageError) as e: + application_package_entity.action_release_channel_remove_version( + action_ctx=action_context, + release_channel="invalid_channel", + version="1.0", + ) + + assert ( + str(e.value) + == f"Release channel invalid_channel is not available in application package {pkg_model.fqn.name}. Available release channels are: (test_channel)." + ) + + remove_version_from_release_channel.assert_not_called() diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index a5493c8192..43d671659e 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -61,6 +61,7 @@ SQL_COMPILATION_ERROR, VERSION_DOES_NOT_EXIST, VERSION_NOT_ADDED_TO_RELEASE_CHANNEL, + VERSION_NOT_IN_RELEASE_CHANNEL, ) from snowflake.connector import DatabaseError, DictCursor, Error from snowflake.connector.errors import ( @@ -3896,3 +3897,166 @@ def test_remove_accounts_from_release_channel_error( ) assert error_message in str(err) + + +def test_add_version_to_release_channel_valid_input_then_success( + mock_use_role, mock_execute_query +): + package_name = "test_package" + release_channel = "test_channel" + version = "v1" + role = "test_role" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + f"alter application package {package_name} modify release channel {release_channel} add version {version}" + ), + ), + ] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.add_version_to_release_channel( + package_name, release_channel, version, role + ) + + +def test_add_version_to_release_channel_with_special_chars_in_names( + mock_use_role, mock_execute_query +): + package_name = "test.package" + release_channel = "test.channel" + version = "v1.0" + role = "test_role" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + f'alter application package "{package_name}" modify release channel "{release_channel}" add version "{version}"' + ), + ), + ] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.add_version_to_release_channel( + package_name, release_channel, version, role + ) + + +@pytest.mark.parametrize( + "error_raised, error_caught, error_message", + [ + ( + ProgrammingError(errno=VERSION_DOES_NOT_EXIST), + UserInputError, + "Version v1 does not exist in application package test_package.", + ), + ( + ProgrammingError(), + InvalidSQLError, + "Failed to add version v1 to release channel test_channel in application package test_package.", + ), + ], +) +@mock.patch(SQL_EXECUTOR_EXECUTE) +def test_add_version_to_release_channel_error( + mock_execute_query, error_raised, error_caught, error_message, mock_use_role +): + mock_execute_query.side_effect = error_raised + + with pytest.raises(error_caught) as err: + sql_facade.add_version_to_release_channel( + "test_package", "test_channel", "v1", "test_role" + ) + + assert error_message in str(err) + + +# same tests but for remove_version_from_release_channel +def test_remove_version_from_release_channel_valid_input_then_success( + mock_use_role, mock_execute_query +): + package_name = "test_package" + release_channel = "test_channel" + version = "v1" + role = "test_role" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + f"alter application package {package_name} modify release channel {release_channel} drop version {version}" + ), + ), + ] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.remove_version_from_release_channel( + package_name, release_channel, version, role + ) + + +def test_remove_version_from_release_channel_with_special_chars_in_names( + mock_use_role, mock_execute_query +): + package_name = "test.package" + release_channel = "test.channel" + version = "v1.0" + role = "test_role" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + f'alter application package "{package_name}" modify release channel "{release_channel}" drop version "{version}"' + ), + ), + ] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.remove_version_from_release_channel( + package_name, release_channel, version, role + ) + + +@pytest.mark.parametrize( + "error_raised, error_caught, error_message", + [ + ( + ProgrammingError(errno=VERSION_NOT_IN_RELEASE_CHANNEL), + UserInputError, + "Version v1 is not found in release channel test_channel.", + ), + ( + ProgrammingError(), + InvalidSQLError, + "Failed to remove version v1 from release channel test_channel in application package test_package.", + ), + ], +) +@mock.patch(SQL_EXECUTOR_EXECUTE) +def test_remove_version_from_release_channel_error( + mock_execute_query, error_raised, error_caught, error_message, mock_use_role +): + mock_execute_query.side_effect = error_raised + + with pytest.raises(error_caught) as err: + sql_facade.remove_version_from_release_channel( + "test_package", "test_channel", "v1", "test_role" + ) + + assert error_message in str(err) diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index d3f7ee4d32..8db8953a4d 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -102,6 +102,12 @@ SQL_FACADE_REMOVE_ACCOUNTS_FROM_RELEASE_CHANNEL = ( f"{SQL_FACADE}.remove_accounts_from_release_channel" ) +SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL = ( + f"{SQL_FACADE}.add_version_to_release_channel" +) +SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL = ( + f"{SQL_FACADE}.remove_version_from_release_channel" +) mock_snowflake_yml_file = dedent( """\