From a539ebfced4ba99bff93ffd054deb2e8476d15eb Mon Sep 17 00:00:00 2001
From: Adam Cain <adam.cain@databricks.com>
Date: Wed, 11 Nov 2020 10:35:23 -0800
Subject: [PATCH 1/3] Initial Unity Catalog changes

* Adds 'unity-catalog' CLIs.
* Squashed 49 commits from SC57404-add-managed-catalog-cli branch.
---
 databricks_cli/cli.py                         |    2 +
 databricks_cli/click_types.py                 |   15 +
 databricks_cli/sdk/service.py                 |    2 +
 databricks_cli/unity_catalog/__init__.py      |    0
 databricks_cli/unity_catalog/api.py           |  229 +++
 databricks_cli/unity_catalog/cli.py           | 1507 +++++++++++++++++
 databricks_cli/utils.py                       |    8 +-
 examples/unity-catalog/create-cred1.json      |    6 +
 examples/unity-catalog/create-dac1.json       |    6 +
 examples/unity-catalog/create-ext-table1.json |   27 +
 examples/unity-catalog/create-loc1.json       |    6 +
 examples/unity-catalog/create-table1.json     |   38 +
 examples/unity-catalog/create-view1.json      |    7 +
 .../unity-catalog/replace-permissions1.json   |   18 +
 examples/unity-catalog/update-catalog.json    |    4 +
 examples/unity-catalog/update-metastore.json  |    4 +
 .../unity-catalog/update-permissions1.json    |   17 +
 examples/unity-catalog/update-schema.json     |    4 +
 examples/unity-catalog/update-table.json      |    4 +
 19 files changed, 1901 insertions(+), 3 deletions(-)
 create mode 100644 databricks_cli/unity_catalog/__init__.py
 create mode 100644 databricks_cli/unity_catalog/api.py
 create mode 100644 databricks_cli/unity_catalog/cli.py
 create mode 100644 examples/unity-catalog/create-cred1.json
 create mode 100644 examples/unity-catalog/create-dac1.json
 create mode 100644 examples/unity-catalog/create-ext-table1.json
 create mode 100644 examples/unity-catalog/create-loc1.json
 create mode 100644 examples/unity-catalog/create-table1.json
 create mode 100644 examples/unity-catalog/create-view1.json
 create mode 100644 examples/unity-catalog/replace-permissions1.json
 create mode 100644 examples/unity-catalog/update-catalog.json
 create mode 100644 examples/unity-catalog/update-metastore.json
 create mode 100644 examples/unity-catalog/update-permissions1.json
 create mode 100644 examples/unity-catalog/update-schema.json
 create mode 100644 examples/unity-catalog/update-table.json

diff --git a/databricks_cli/cli.py b/databricks_cli/cli.py
index 0b4f6ecf..6960ee9a 100644
--- a/databricks_cli/cli.py
+++ b/databricks_cli/cli.py
@@ -41,6 +41,7 @@
 from databricks_cli.instance_pools.cli import instance_pools_group
 from databricks_cli.pipelines.cli import pipelines_group
 from databricks_cli.repos.cli import repos_group
+from databricks_cli.unity_catalog.cli import unity_catalog_group
 
 
 @click.group(context_settings=CONTEXT_SETTINGS)
@@ -67,6 +68,7 @@ def cli():
 cli.add_command(instance_pools_group, name="instance-pools")
 cli.add_command(pipelines_group, name='pipelines')
 cli.add_command(repos_group, name='repos')
+cli.add_command(unity_catalog_group, name='unity-catalog')
 
 if __name__ == "__main__":
     cli()
diff --git a/databricks_cli/click_types.py b/databricks_cli/click_types.py
index ff508a74..e5cfa092 100644
--- a/databricks_cli/click_types.py
+++ b/databricks_cli/click_types.py
@@ -106,6 +106,21 @@ class PipelineIdClickType(ParamType):
     help = 'Delta Pipeline ID'
 
 
+class MetastoreIdClickType(ParamType):
+    name = 'METASTORE_ID'
+    help = 'ID of the Metastore'
+
+
+class DacIdClickType(ParamType):
+    name = 'DAC_ID'
+    help = 'ID of the desired data access configuration'
+
+
+class WorkspaceIdClickType(ParamType):
+    name = 'WORKSPACE_ID'
+    help = 'ID of the Workspace'
+
+
 class OneOfOption(Option):
     def __init__(self, *args, **kwargs):
         self.one_of = kwargs.pop('one_of')
diff --git a/databricks_cli/sdk/service.py b/databricks_cli/sdk/service.py
index 5678be2d..cb044d20 100755
--- a/databricks_cli/sdk/service.py
+++ b/databricks_cli/sdk/service.py
@@ -1063,6 +1063,8 @@ def stop(self, pipeline_id=None, headers=None):
 
         return self.client.perform_query('POST', '/pipelines/{pipeline_id}/stop'.format(pipeline_id=pipeline_id),
                                          data=_data, headers=headers)
+
+
 class ReposService(object):
     def __init__(self, client):
         self.client = client
diff --git a/databricks_cli/unity_catalog/__init__.py b/databricks_cli/unity_catalog/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/databricks_cli/unity_catalog/api.py b/databricks_cli/unity_catalog/api.py
new file mode 100644
index 00000000..589531ad
--- /dev/null
+++ b/databricks_cli/unity_catalog/api.py
@@ -0,0 +1,229 @@
+# Databricks CLI
+# Copyright 2017 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from databricks_cli.sdk import UnityCatalogService
+
+
+class UnityCatalogApi(object):
+    def __init__(self, api_client):
+        self.client = UnityCatalogService(api_client)
+
+    # Metastore APIs
+
+    def create_metastore(self, name, storage_root):
+        return self.client.create_metastore(name, storage_root)
+
+    def list_metastores(self):
+        return self.client.list_metastores()
+
+    def get_metastore(self, metastore_id):
+        return self.client.get_metastore(metastore_id)
+
+    def update_metastore(self, metastore_id, metastore_spec):
+        return self.client.update_metastore(metastore_id, metastore_spec)
+
+    def delete_metastore(self, metastore_id, force):
+        return self.client.delete_metastore(metastore_id, force)
+
+    def get_metastore_summary(self):
+        return self.client.get_metastore_summary()
+
+    def create_metastore_assignment(self, workspace_id, metastore_id, default_catalog_name):
+        return self.client.create_metastore_assignment(workspace_id, metastore_id,
+                                                       default_catalog_name)
+
+    def update_metastore_assignment(self, workspace_id, metastore_id, default_catalog_name):
+        return self.client.update_metastore_assignment(workspace_id, metastore_id,
+                                                       default_catalog_name)
+
+    # Catalog APIs
+
+    def create_catalog(self, catalog_name, comment):
+        return self.client.create_catalog(catalog_name, comment)
+
+    def list_catalogs(self):
+        return self.client.list_catalogs()
+
+    def get_catalog(self, name):
+        return self.client.get_catalog(name)
+
+    def update_catalog(self, name, catalog_spec):
+        return self.client.update_catalog(name, catalog_spec)
+
+    def delete_catalog(self, catalog_name):
+        return self.client.delete_catalog(catalog_name)
+
+    # Schema APIs
+
+    def create_schema(self, catalog_name, schema_name, comment):
+        return self.client.create_schema(catalog_name, schema_name, comment)
+
+    def list_schemas(self, catalog_name, name_pattern):
+        return self.client.list_schemas(catalog_name, name_pattern)
+
+    def get_schema(self, full_name):
+        return self.client.get_schema(full_name)
+
+    def update_schema(self, full_name, schema_spec):
+        return self.client.update_schema(full_name, schema_spec)
+
+    def delete_schema(self, schema_full_name):
+        return self.client.delete_schema(schema_full_name)
+
+    # Table APIs
+
+    def create_table(self, table_spec):
+        return self.client.create_table(table_spec)
+
+    def list_tables(self, catalog_name, schema_name, name_pattern):
+        return self.client.list_tables(catalog_name, schema_name, name_pattern)
+
+    def list_tables_bulk(self, catalog_name):
+        return self.client.list_tables_bulk(catalog_name)
+
+    def get_table(self, full_name):
+        return self.client.get_table(full_name)
+
+    def update_table(self, full_name, table_spec):
+        return self.client.update_table(full_name, table_spec)
+
+    def delete_table(self, table_full_name):
+        return self.client.delete_table(table_full_name)
+
+    # Data Access Configuration APIs
+
+    def create_dac(self, metastore_id, dac):
+        return self.client.create_dac(metastore_id, dac)
+
+    def list_dacs(self, metastore_id):
+        return self.client.list_dacs(metastore_id)
+
+    def get_dac(self, metastore_id, dac_id):
+        return self.client.get_dac(metastore_id, dac_id)
+
+    def delete_dac(self, metastore_id, dac_id):
+        return self.client.delete_dac(metastore_id, dac_id)
+
+    def create_root_credentials(self, root_creds):
+        return self.client.create_root_credentials(root_creds)
+
+    # Storage Credentials
+
+    def create_storage_credential(self, cred_spec):
+        return self.client.create_storage_credential(cred_spec)
+
+    def list_storage_credentials(self, name_pattern):
+        return self.client.list_storage_credentials(name_pattern)
+
+    def get_storage_credential(self, name):
+        return self.client.get_storage_credential(name)
+
+    def update_storage_credential(self, name, cred_spec):
+        return self.client.update_storage_credential(name, cred_spec)
+
+    def delete_storage_credential(self, name):
+        return self.client.delete_storage_credential(name)
+
+    # External Locations
+
+    def create_external_location(self, loc_spec):
+        return self.client.create_external_location(loc_spec)
+
+    def list_external_locations(self):
+        return self.client.list_external_locations()
+
+    def get_external_location(self, name):
+        return self.client.get_external_location(name)
+
+    def update_external_location(self, name, force, loc_spec):
+        return self.client.update_external_location(name, force, loc_spec)
+
+    def delete_external_location(self, name, force):
+        return self.client.delete_external_location(name, force)
+
+    # Permissions APIs
+
+    def get_permissions(self, sec_type, sec_name):
+        return self.client.get_permissions(sec_type, sec_name)
+
+    def update_permissions(self, sec_type, sec_name, diff_spec):
+        return self.client.update_permissions(sec_type, sec_name, diff_spec)
+
+    def replace_permissions(self, sec_type, sec_name, perm_spec):
+        return self.client.replace_permissions(sec_type, sec_name, perm_spec)
+
+    # Share APIs
+
+    def create_share(self, name):
+        return self.client.create_share(name)
+
+    def list_shares(self):
+        return self.client.list_shares()
+
+    def get_share(self, name, include_shared_data):
+        return self.client.get_share(name, include_shared_data)
+
+    def update_share(self, name, share_spec):
+        return self.client.update_share(name, share_spec)
+
+    def delete_share(self, name):
+        return self.client.delete_share(name)
+
+    # Recipient APIs
+
+    def create_recipient(self, name, comment):
+        return self.client.create_recipient(name, comment)
+
+    def list_recipients(self):
+        return self.client.list_recipients()
+
+    def get_recipient(self, name):
+        return self.client.get_recipient(name)
+
+    def rotate_recipient_token(self, name, existing_token_expire_in_seconds):
+        return self.client.rotate_recipient_token(name, existing_token_expire_in_seconds)
+
+    def get_recipient_share_permissions(self, name):
+        return self.client.get_recipient_share_permissions(name)
+
+    def delete_recipient(self, name):
+        return self.client.delete_recipient(name)
+
+    # Provider APIs
+
+    def create_provider(self, name, comment, provider_profile):
+        return self.client.create_provider(name, comment, provider_profile)
+
+    def list_providers(self):
+        return self.client.list_providers()
+
+    def get_provider(self, name):
+        return self.client.get_provider(name)
+
+    def update_provider(self, name, new_name=None, comment=None, provider_profile=None):
+        return self.client.update_provider(name, new_name, comment, provider_profile)
+
+    def delete_provider(self, name):
+        return self.client.delete_provider(name)
+
+    def list_provider_shares(self, name):
+        return self.client.list_provider_shares(name)
diff --git a/databricks_cli/unity_catalog/cli.py b/databricks_cli/unity_catalog/cli.py
new file mode 100644
index 00000000..2c667245
--- /dev/null
+++ b/databricks_cli/unity_catalog/cli.py
@@ -0,0 +1,1507 @@
+# Databricks CLI
+# Copyright 2017 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import MetastoreIdClickType, DacIdClickType, \
+    WorkspaceIdClickType, JsonClickType, OneOfOption
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, pretty_format, json_cli_base
+from databricks_cli.version import print_version_callback, version
+
+
+# Encode UTF-8 strings in JSON blobs
+def mc_pretty_format(json):
+    return pretty_format(json, encode_utf8=True)
+
+
+#################  Metastore Commands  #####################
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a metastore.')
+@click.option('--name', required=True, help='Name of the new metastore.')
+@click.option('--storage-root', required=True,
+              help='Storage root URL for the new metastore.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_metastore_cli(api_client, name, storage_root):
+    """
+    Create new metastore specified by the JSON input.
+
+    Calls the 'createMetastore' RPC endpoint of the Unity Catalog service.
+    Returns the properties of the newly-created metastore.
+
+    """
+    metastore_json = UnityCatalogApi(api_client).create_metastore(name, storage_root)
+    click.echo(mc_pretty_format(metastore_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List metastores.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_metastores_cli(api_client):
+    """
+    List metastores.
+
+    Calls the 'listMetastores' RPC endpoint of the Unity Catalog service.
+    Returns array of MetastoreInfos.
+
+    """
+    metastores_json = UnityCatalogApi(api_client).list_metastores()
+    click.echo(mc_pretty_format(metastores_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a metastore.')
+@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_metastore_cli(api_client, metastore_id):
+    """
+    Get a metastore.
+
+    Calls the 'getMetastore' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    metastore_json = UnityCatalogApi(api_client).get_metastore(metastore_id)
+    click.echo(mc_pretty_format(metastore_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a metastore.')
+@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/admin/metastores'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_metastore_cli(api_client, metastore_id, json_file, json):
+    """
+    Update a metastore.
+
+    Calls the 'updateMetastore' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_metastore(metastore_id, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a metastore.')
+@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to delete.')
+@click.option('--force', '-f', is_flag=True, default=False)
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_metastore_cli(api_client, metastore_id, force):
+    """
+    Delete a metastore.
+
+    Calls the 'deleteMetastore' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_metastore(metastore_id, force)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get summary info of current metastore.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def metastore_summary_cli(api_client):
+    """
+    Get metastore summary.
+
+    Calls the 'getMetastoreSummary' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    summary_json = UnityCatalogApi(api_client).get_metastore_summary()
+    click.echo(mc_pretty_format(summary_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Assign a metastore to the current workspace.')
+@click.option('--workspace-id', 'workspace_id', required=True, type=WorkspaceIdClickType(),
+              help='Unique identifier of the metastore to assign.')
+@click.option('--metastore-id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to assign.')
+@click.option('--default-catalog-name', 'default_catalog_name', required=False, default='main',
+              help='Name of the default catalog to use with the metastore.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def assign_metastore_cli(api_client, workspace_id, metastore_id, default_catalog_name):
+    """
+    Assign a metastore to the current workspace.
+
+    Calls the 'createMetastoreAssignment' RPC endpoint of the Unity Catalog service.
+    If that fails due to the workspace already having a Metastore assigned, it calls
+    the 'updateMetastoreAssignment' endpoint.
+    Returns nothing.
+
+    """
+    resp = UnityCatalogApi(api_client).create_metastore_assignment(workspace_id, metastore_id,
+                                                                   default_catalog_name)
+    # resp will just be an empty object ('{}') but it's good to print *something*
+    click.echo(mc_pretty_format(resp))
+
+
+##############  Catalog Commands  ##############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new catalog.')
+@click.option('--name', required=True, help='Name of new catalog.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_catalog_cli(api_client, name, comment):
+    """
+    Create a new catalog.
+
+    Calls the 'createCatalog' RPC endpoint of the Unity Catalog service.
+    Returns the CatalogInfo for the newly-created catalog.
+
+    """
+    catalog_json = UnityCatalogApi(api_client).create_catalog(name, comment)
+    click.echo(mc_pretty_format(catalog_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List catalogs.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_catalogs_cli(api_client):
+    """
+    List catalogs.
+
+    Calls the 'listCatalogs' RPC endpoint of the Unity Catalog service.
+    Returns array of CatalogInfos.
+
+    """
+    catalogs_json = UnityCatalogApi(api_client).list_catalogs()
+    click.echo(mc_pretty_format(catalogs_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a catalog.')
+@click.option('--name', required=True,
+              help='Name of the catalog to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_catalog_cli(api_client, name):
+    """
+    Get a catalog.
+
+    Calls the 'getCatalog' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    catalog_json = UnityCatalogApi(api_client).get_catalog(name)
+    click.echo(mc_pretty_format(catalog_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a catalog.')
+@click.option('--name', required=True,
+              help='Name of the catalog to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/catalogs'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_catalog_cli(api_client, name, json_file, json):
+    """
+    Update a catalog.
+
+    Calls the 'updateCatalog' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_catalog(name, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a catalog.')
+@click.option('--name', required=True,
+              help='Name of the catalog to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_catalog_cli(api_client, name):
+    """
+    Delete a catalog.
+
+    Calls the 'deleteCatalog' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_catalog(name)
+
+
+#############  Schema Commands ##############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new schema.')
+@click.option('--catalog-name', required=True, help='Parent catalog of new schema.')
+@click.option('--name', required=True,
+              help='Name of new schema, relative to parent catalog.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_schema_cli(api_client, catalog_name, name, comment):
+    """
+    Create a new schema in the specified catalog.
+
+    Calls the 'createSchema' RPC endpoint of the Unity Catalog service.
+    Returns the SchemaInfo for the newly-created schema.
+
+    """
+    schema_json = UnityCatalogApi(api_client).create_schema(catalog_name, name, comment)
+    click.echo(mc_pretty_format(schema_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List schemas.')
+@click.option('--catalog-name', required=True,
+              help='Name of the parent catalog for schemas of interest.')
+@click.option('--name-pattern', default=None,
+              help='SQL LIKE pattern that the schema name must match to be in list.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_schemas_cli(api_client, catalog_name, name_pattern):
+    """
+    List schemas.
+
+    Calls the 'listSchemas' RPC endpoint of the Unity Catalog service.
+    Returns array of SchemaInfos.
+
+    """
+    schemas_json = UnityCatalogApi(api_client).list_schemas(catalog_name, name_pattern)
+    click.echo(mc_pretty_format(schemas_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a schema.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>) of the schema to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_schema_cli(api_client, full_name):
+    """
+    Get a schema.
+
+    Calls the 'getSchema' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    schema_json = UnityCatalogApi(api_client).get_schema(full_name)
+    click.echo(mc_pretty_format(schema_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a schema.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>) of the schema to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/schemas'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_schema_cli(api_client, full_name, json_file, json):
+    """
+    Update a schema.
+
+    Calls the 'updateSchema' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_schema(full_name, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a schema.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>) of the schema to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_schema_cli(api_client, full_name):
+    """
+    Delete a schema.
+
+    Calls the 'deleteSchema' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_schema(full_name)
+
+
+##############  Table Commands  #################
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a table.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/tables'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_table_cli(api_client, json_file, json):
+    """
+    Create new table specified by the JSON input.
+
+    Calls the 'createTable' RPC endpoint of the Unity Catalog service.
+    Returns the properties of the newly-created table.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_table(json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List tables.')
+@click.option('--catalog-name', required=True,
+              help='Name of the parent catalog for tables of interest.')
+@click.option('--schema-name', required=True,
+              help='Name of the parent schema for tables of interest.')
+@click.option('--name-pattern', default=None,
+              help='SQL LIKE pattern that the table name must match to be in list.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_tables_cli(api_client, catalog_name, schema_name, name_pattern):
+    """
+    List tables.
+
+    Calls the 'listTables' RPC endpoint of the Unity Catalog service.
+    Returns array of TableInfos.
+
+    """
+    tables_json = UnityCatalogApi(api_client).list_tables(catalog_name, schema_name, name_pattern)
+    click.echo(mc_pretty_format(tables_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List tables bulk.')
+@click.option('--catalog-name', required=True,
+              help='Name of the parent catalog for tables of interest.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_tables_bulk_cli(api_client, catalog_name):
+    """
+    List tables bulk.
+
+    Calls the 'listTablesBulk' RPC endpoint of the Unity Catalog service.
+    Returns array of TableSummarys.
+
+    """
+    tables_json = UnityCatalogApi(api_client).list_tables_bulk(catalog_name)
+    click.echo(mc_pretty_format(tables_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a table.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>.<table>) of the table to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_table_cli(api_client, full_name):
+    """
+    Get a table.
+
+    Calls the 'getTable' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    table_json = UnityCatalogApi(api_client).get_table(full_name)
+    click.echo(mc_pretty_format(table_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a table.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>.<table>) of the table to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/tables'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_table_cli(api_client, full_name, json_file, json):
+    """
+    Update a table.
+
+    Calls the 'updateTable' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_table(full_name, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a table.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>.<table>) of the table to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_table_cli(api_client, full_name):
+    """
+    Delete a table.
+
+    Calls the 'deleteTable' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_table(full_name)
+
+
+#############  Data Access Configuration Commands  ############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create data access configuration.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/data-access-configurations'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_dac_cli(api_client, metastore_id, json_file, json):
+    """
+    Create new data access configuration.
+
+    Calls the 'createDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the properties of the newly-created DAC.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_dac(metastore_id, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List data access configurations.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC(s).')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_dacs_cli(api_client, metastore_id):
+    """
+    List data access configurations.
+
+    Calls the 'listDataAccessConfigurations' RPC endpoint of the Unity Catalog service.
+    Returns array of DataAccessConfigurations.
+
+    """
+    dacs_json = UnityCatalogApi(api_client).list_dacs(metastore_id)
+    click.echo(mc_pretty_format(dacs_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get data access configuration.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC.')
+@click.option('--dac-id', required=True, type=DacIdClickType(),
+              help='Data access configuration ID.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_dac_cli(api_client, metastore_id, dac_id):
+    """
+    Get data access configuration details.
+
+    Calls the 'getDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
+    Returns details of the DAC specified by its id (TODO: lookup by DAC name?).
+
+    """
+    dac_json = UnityCatalogApi(api_client).get_dac(metastore_id, dac_id)
+    click.echo(mc_pretty_format(dac_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete data access configuration.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC.')
+@click.option('--dac-id', required=True, type=DacIdClickType(),
+              help='Data access configuration ID.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_dac_cli(api_client, metastore_id, dac_id):
+    """
+    Delete data access configuration details.
+
+    Calls the 'deleteDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
+
+    """
+    UnityCatalogApi(api_client).delete_dac(metastore_id, dac_id)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create temporary credentials for storage root access.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/root-credentials'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_root_credentials_cli(api_client, json_file, json):
+    """
+    Create new temporary credentials (token) for storage root access.
+
+    Calls the 'createRootCredentials' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the newly-created temporary credentials.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_root_credentials(json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create storage credential.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/storage-credentials'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_credential_cli(api_client, json_file, json):
+    """
+    Create new storage credential.
+
+    Calls the 'createStorageCredential' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the properties of the newly-created Storage Credential.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_storage_credential(json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List storage credentials.')
+@click.option('--name-pattern', default=None,
+              help='SQL LIKE pattern that the credential name must match to be in list.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_credentials_cli(api_client, name_pattern):
+    """
+    List storage credentials.
+
+    Calls the 'listStorageCredentials' RPC endpoint of the Unity Catalog service.
+    Returns array of StorageCredentials.
+
+    """
+    creds_json = UnityCatalogApi(api_client).list_storage_credentials(name_pattern)
+    click.echo(mc_pretty_format(creds_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a storage credential.')
+@click.option('--name', required=True,
+              help='Name of the storage credential to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_credential_cli(api_client, name):
+    """
+    Get a storage credential.
+
+    Calls the 'getStorageCredential' RPC endpoint of the Unity Catalog service.
+    Returns an StorageCredential object.
+
+    """
+    cred_json = UnityCatalogApi(api_client).get_storage_credential(name)
+    click.echo(mc_pretty_format(cred_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a storage credential.')
+@click.option('--name', required=True,
+              help='Name of the storage credential to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/storage-credentials'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_credential_cli(api_client, name, json_file, json):
+    """
+    Update a storage credential.
+
+    Calls the 'updateStorageCredential' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_storage_credential(name, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a storage credential.')
+@click.option('--name', required=True,
+              help='Name of the storage credential to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_credential_cli(api_client, name):
+    """
+    Delete a storage credential.
+
+    Calls the 'deleteStorageCredential' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_storage_credential(name)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create External Location.')
+@click.option('--name', default=None,
+              help='Name of new external location')
+@click.option('--url', default=None,
+              help='Path URL for the new external location')
+@click.option('--storage-credential-name', default=None,
+              help='Name of storage credential to use with new external location')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/external-locations'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_location_cli(api_client, name, url, storage_credential_name, json_file, json):
+    """
+    Create new external location.
+
+    Calls the 'createExternalLocation' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the properties of the newly-created Storage Credential.
+
+    """
+    if (name is not None) and (url is not None) and (storage_credential_name is not None):
+        if (json_file is not None) or (json is not None):
+            raise ValueError('Cannot specify JSON if both name and url are given')
+        data = {"name": name, "url": url, "credential_name": storage_credential_name}
+        loc_json = UnityCatalogApi(api_client).create_external_location(data)
+        click.echo(mc_pretty_format(loc_json))
+    elif (json is None) and (json_file is None):
+        raise ValueError('Must provide name, url and storage-credential-name or use JSON specification')
+    else:
+        json_cli_base(json_file, json,
+                      lambda json: UnityCatalogApi(api_client).create_external_location(json),
+                      encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List external locations.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_locations_cli(api_client, ):
+    """
+    List external locations.
+
+    Calls the 'listExternalLocations' RPC endpoint of the Unity Catalog service.
+    Returns array of ExternalLocations.
+
+    """
+    locs_json = UnityCatalogApi(api_client).list_external_locations()
+    click.echo(mc_pretty_format(locs_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get an external location.')
+@click.option('--name', required=True,
+              help='Name of the external location to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_location_cli(api_client, name):
+    """
+    Get an external location.
+
+    Calls the 'getExternalLocation' RPC endpoint of the Unity Catalog service.
+    Returns an ExternalLocation object.
+
+    """
+    loc_json = UnityCatalogApi(api_client).get_external_location(name)
+    click.echo(mc_pretty_format(loc_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update an external location.')
+@click.option('--name', required=True,
+              help='Name of the external location to update.')
+@click.option('--force', '-f', is_flag=True, default=False,
+              help='Force update even if location has dependent tables/mounts')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/external-locations'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_location_cli(api_client, name, force, json_file, json):
+    """
+    Update an external location.
+
+    Calls the 'updateExternalLocation' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_external_location(name, force, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete an external location.')
+@click.option('--name', required=True,
+              help='Name of the external location to delete.')
+@click.option('--force', '-f', is_flag=True, default=False,
+              help='Force deletion even if location has dependent tables/mounts')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_location_cli(api_client, name, force):
+    """
+    Delete an external location.
+
+    Calls the 'deleteExternalLocation' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_external_location(name, force)
+
+
+PERMISSIONS_OBJ_TYPES = [
+    'catalog', 'schema', 'table', 'share', 'storage-credential', 'external-location'
+]
+
+
+def _get_perm_securable_name_and_type(catalog_name, schema_full_name, table_full_name,
+                                      share_name, credential_name, location_name):
+    if catalog_name:
+        return ('catalog', catalog_name)
+    elif schema_full_name:
+        return ('schema', schema_full_name)
+    elif table_full_name:
+        return ('table', table_full_name)
+    elif share_name:
+        return ('share', share_name)
+    elif credential_name:
+        return ('storage-credential', credential_name)
+    else:
+        return ('external-location', location_name)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get permissions on a securable.')
+@click.option('--catalog', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of catalog of interest')
+@click.option('--schema', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of schema of interest')
+@click.option('--table', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of table of interest')
+@click.option('--share', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the share of interest')
+@click.option('--storage-credential', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the storage credential of interest')
+@click.option('--external-location', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the external location of interest')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_permissions_cli(api_client, catalog, schema, table, share, storage_credential,
+                        external_location):
+    """
+    Get permissions on a securable.
+
+    Calls the 'getPermissions' RPC endpoint of the Unity Catalog service.
+    Returns PermissionsList for the requested securable.
+
+    """
+    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table, share,
+                                                           storage_credential, external_location)
+
+    perm_json = UnityCatalogApi(api_client).get_permissions(sec_type, sec_name)
+    click.echo(mc_pretty_format(perm_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='update permissions on a securable.')
+@click.option('--catalog', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of catalog of interest')
+@click.option('--schema', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of schema of interest')
+@click.option('--table', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of table of interest')
+@click.option('--share', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the share of interest')
+@click.option('--storage-credential', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the storage credential of interest')
+@click.option('--external-location', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the external location of interest')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON of permissions change to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/permissions'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_permissions_cli(api_client, catalog, schema, table, share, storage_credential,
+                           external_location, json_file, json):
+    """
+    Update permissions on a securable.
+
+    Calls the 'updatePermissions' RPC endpoint of the Unity Catalog service.
+    Returns updated PermissionsList for the requested securable.
+
+    """
+    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table, share,
+                                                           storage_credential, external_location)
+
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_permissions(sec_type, sec_name,
+                                                                              json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='replace permissions on a securable.')
+@click.option('--catalog', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of catalog of interest')
+@click.option('--schema', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of schema of interest')
+@click.option('--table', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of table of interest')
+# shares not supported for replace permissions
+@click.option('--credential', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the storage credential of interest')
+@click.option('--location', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the external location of interest')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON of permissions to PUT.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/permissions'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def replace_permissions_cli(api_client, catalog, schema, table, credential, location,
+                            json_file, json):
+    """
+    Replace permissions on a securable.
+
+    Calls the 'replacePermissions' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    share = None  # shares not supported for replace permissions
+
+    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table, share,
+                                                           credential, location)
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).replace_permissions(sec_type, sec_name,
+                                                                               json),
+                  encode_utf8=True)
+
+##############  Share Commands  ##############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new share.')
+@click.option('--name', required=True, help='Name of new share.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_share_cli(api_client, name):
+    """
+    Create a new share.
+
+    Calls the 'createShare' RPC endpoint of the Unity Catalog service.
+    Returns the ShareInfo for the newly-created share.
+
+    """
+    share_json = UnityCatalogApi(api_client).create_share(name)
+    click.echo(mc_pretty_format(share_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List shares.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_shares_cli(api_client):
+    """
+    List shares.
+
+    Calls the 'listShares' RPC endpoint of the Unity Catalog service.
+    Returns array of ShareInfos.
+
+    """
+    shares_json = UnityCatalogApi(api_client).list_shares()
+    click.echo(mc_pretty_format(shares_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a share.')
+@click.option('--name', required=True,
+              help='Name of the share to get.')
+@click.option('--include-shared-data', default=True,
+              help='Whether to include shared data in the response.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_share_cli(api_client, name, include_shared_data):
+    """
+    Get a share.
+
+    Calls the 'getShare' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    share_json = UnityCatalogApi(api_client).get_share(name, include_shared_data)
+    click.echo(mc_pretty_format(share_json))
+
+
+def shared_data_object(name):
+    return {'name': name, 'data_object_type': 'TABLE'}
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a share.')
+@click.option('--name', required=True,
+              help='Name of the share to update.')
+@click.option('--add-table', default=None, multiple=True,
+              help='Full name of table to add to share')
+@click.option('--remove-table', default=None, multiple=True,
+              help='Full name of table to remove from share')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/shares'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_share_cli(api_client, name, add_table, remove_table, json_file, json):
+    """
+    Update a share.
+
+    Calls the 'updateShare' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    if len(add_table) > 0 or len(remove_table) > 0:
+        updates = []
+        for a in add_table:
+            updates.append({'action': 'ADD', 'data_object': shared_data_object(a)})
+        for r in remove_table:
+            updates.append({'action': 'REMOVE', 'data_object': shared_data_object(r)})
+        d = {'updates': updates}
+        share_json = UnityCatalogApi(api_client).update_share(name, d)
+        click.echo(mc_pretty_format(share_json))
+    else:
+        json_cli_base(json_file, json,
+                      lambda json: UnityCatalogApi(api_client).update_share(name, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a share.')
+@click.option('--name', required=True,
+              help='Name of the share to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_share_cli(api_client, name):
+    """
+    Delete a share.
+
+    Calls the 'deleteShare' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_share(name)
+
+
+##############  Recipient Commands  ##############
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new recipient.')
+@click.option('--name', required=True, help='Name of new recipient.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_recipient_cli(api_client, name, comment):
+    """
+    Create a new recipient.
+
+    Calls the 'createRecipient' RPC endpoint of the Unity Catalog service.
+    Returns the RecipientInfo for the newly-created recipient.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).create_recipient(name, comment)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List recipients.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_recipients_cli(api_client):
+    """
+    List recipients.
+
+    Calls the 'listRecipients' RPC endpoint of the Unity Catalog service.
+    Returns array of RecipientInfos.
+
+    """
+    recipients_json = UnityCatalogApi(api_client).list_recipients()
+    click.echo(mc_pretty_format(recipients_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a recipient.')
+@click.option('--name', required=True,
+              help='Name of the recipient to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_recipient_cli(api_client, name):
+    """
+    Get a recipient.
+
+    Calls the 'getRecipient' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).get_recipient(name)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Rotate token for the recipient.')
+@click.option('--name', required=True, help='Name of new recipient.')
+@click.option('--existing-token-expire-in-seconds', default=None, required=False,
+              help='Expire the existing token in number of seconds from now, 0 to expire it immediately.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def rotate_recipient_token_cli(api_client, name, existing_token_expire_in_seconds):
+    """
+    Rotate recipient token.
+
+    Calls the 'rotateRecipientToken' RPC endpoint of the Unity Catalog service.
+    Returns the RecipientInfo for the recipient with rotated tokens.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).rotate_recipient_token(name, existing_token_expire_in_seconds)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get share permissions of a recipient.')
+@click.option('--name', required=True,
+              help='Name of the recipient.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_recipient_share_permissions_cli(api_client, name):
+    """
+    Get a recipient's share permissions.
+
+    Calls the 'getRecipientSharePermissions' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).get_recipient_share_permissions(name)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a recipient.')
+@click.option('--name', required=True,
+              help='Name of the recipient to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_recipient_cli(api_client, name):
+    """
+    Delete a recipient.
+
+    Calls the 'deleteRecipient' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_recipient(name)
+
+
+##############  Provider Commands  ##############
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a provider.')
+@click.option('--name', required=True, help='Name of the new provider.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON provider profile.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/providers'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_provider_cli(api_client, name, comment, json_file, json):
+    """
+    Create a provider.
+
+    Calls the 'createProvider' RPC endpoint of the Unity Catalog service.
+    Returns the created provider info.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_provider(name, comment, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List providers.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_providers_cli(api_client):
+    """
+    List providers.
+
+    Calls the 'listProviders' RPC endpoint of the Unity Catalog service.
+    Returns array of ProviderInfos.
+
+    """
+    proviers_json = UnityCatalogApi(api_client).list_providers()
+    click.echo(mc_pretty_format(proviers_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a provider.')
+@click.option('--name', required=True,
+              help='Name of the provider to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_provider_cli(api_client, name):
+    """
+    Get a provider.
+
+    Calls the 'getProvider' RPC endpoint of the Unity Catalog service.
+    Returns ProviderInfo.
+
+    """
+    provier_json = UnityCatalogApi(api_client).get_provider(name)
+    click.echo(mc_pretty_format(provier_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a provider.')
+@click.option('--name', required=True, help='Name of the provider to update.')
+@click.option('--new_name', default=None, help='New name of the provider.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON provider profile.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/providers'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_provider_cli(api_client, name, new_name, comment, json_file, json):
+    """
+    Update a provider.
+
+    Calls the 'updateProvider' RPC endpoint of the Unity Catalog service.
+    Returns the updated provider info.
+
+    """
+    if json is None and json_file is None:
+        updated_provider = UnityCatalogApi(api_client).update_provider(name, new_name, comment)
+        click.echo(mc_pretty_format(updated_provider))
+    else:
+        json_cli_base(json_file, json,
+                      lambda json: UnityCatalogApi(api_client).update_provider(name, new_name,
+                                                                           comment, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List shares of a provider.')
+@click.option('--name', required=True,
+              help='Name of the provider.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_provider_shares_cli(api_client, name):
+    """
+    List a provider's shares.
+
+    Calls the 'listProviderShares' RPC endpoint of the Unity Catalog service.
+    Returns array of ProviderShare.
+
+    """
+    shares_json = UnityCatalogApi(api_client).list_provider_shares(name)
+    click.echo(mc_pretty_format(shares_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a provider.')
+@click.option('--name', required=True,
+              help='Name of the provider to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_provider_cli(api_client, name):
+    """
+    Delete a provider.
+
+    Calls the 'deleteProvider' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_provider(name)
+
+
+@click.group(context_settings=CONTEXT_SETTINGS,
+             help='Utility to interact with Databricks unity-catalog.\n\n' +
+             '**********************************************************************\n' +
+             'WARNING: these commands are EXPERIMENTAL and not officially supported.\n' +
+             '**********************************************************************',
+             hidden=True)
+@click.option('--version', '-v', is_flag=True, callback=print_version_callback,
+              expose_value=False, is_eager=True, help=version)
+@debug_option
+@profile_option
+@eat_exceptions
+def unity_catalog_group():  # pragma: no cover
+    """
+    Utility to interact with Databricks unity-catalog.
+    """
+    pass
+
+
+# Metastore cmds:
+unity_catalog_group.add_command(create_metastore_cli, name='create-metastore')
+unity_catalog_group.add_command(list_metastores_cli, name='list-metastores')
+unity_catalog_group.add_command(get_metastore_cli, name='get-metastore')
+unity_catalog_group.add_command(update_metastore_cli, name='update-metastore')
+unity_catalog_group.add_command(delete_metastore_cli, name='delete-metastore')
+unity_catalog_group.add_command(metastore_summary_cli, name='metastore-summary')
+unity_catalog_group.add_command(assign_metastore_cli, name='assign-metastore')
+# Catalogs cmds:
+unity_catalog_group.add_command(create_catalog_cli, name='create-catalog')
+unity_catalog_group.add_command(list_catalogs_cli, name='list-catalogs')
+unity_catalog_group.add_command(get_catalog_cli, name='get-catalog')
+unity_catalog_group.add_command(update_catalog_cli, name='update-catalog')
+unity_catalog_group.add_command(delete_catalog_cli, name='delete-catalog')
+# Schema cmds:
+unity_catalog_group.add_command(create_schema_cli, name='create-schema')
+unity_catalog_group.add_command(list_schemas_cli, name='list-schemas')
+unity_catalog_group.add_command(get_schema_cli, name='get-schema')
+unity_catalog_group.add_command(update_schema_cli, name='update-schema')
+unity_catalog_group.add_command(delete_schema_cli, name='delete-schema')
+# Table cmds:
+unity_catalog_group.add_command(create_table_cli, name='create-table')
+unity_catalog_group.add_command(list_tables_cli, name='list-tables')
+unity_catalog_group.add_command(list_tables_bulk_cli, name='list-tables-bulk')
+unity_catalog_group.add_command(get_table_cli, name='get-table')
+unity_catalog_group.add_command(update_table_cli, name='update-table')
+unity_catalog_group.add_command(delete_table_cli, name='delete-table')
+# DAC cmds:
+unity_catalog_group.add_command(create_dac_cli, name='create-dac')
+unity_catalog_group.add_command(list_dacs_cli, name='list-dacs')
+unity_catalog_group.add_command(get_dac_cli, name='get-dac')
+unity_catalog_group.add_command(delete_dac_cli, name='delete-dac')
+# Credential cmds:
+unity_catalog_group.add_command(create_credential_cli, name='create-storage-credential')
+unity_catalog_group.add_command(list_credentials_cli, name='list-storage-credentials')
+unity_catalog_group.add_command(get_credential_cli, name='get-storage-credential')
+unity_catalog_group.add_command(update_credential_cli, name='update-storage-credential')
+unity_catalog_group.add_command(delete_credential_cli, name='delete-storage-credential')
+# External Location cmds:
+unity_catalog_group.add_command(create_location_cli, name='create-external-location')
+unity_catalog_group.add_command(list_locations_cli, name='list-external-locations')
+unity_catalog_group.add_command(get_location_cli, name='get-external-location')
+unity_catalog_group.add_command(update_location_cli, name='update-external-location')
+unity_catalog_group.add_command(delete_location_cli, name='delete-external-location')
+# Permissions cmds:
+unity_catalog_group.add_command(get_permissions_cli, name='get-permissions')
+unity_catalog_group.add_command(update_permissions_cli, name='update-permissions')
+# replacePermissions endpoint not implemented on MC yet
+# unity_catalog_group.add_command(replace_permissions_cli, name='replace-permissions')
+unity_catalog_group.add_command(create_root_credentials_cli, name='create-root-credentials')
+# Share cmds:
+unity_catalog_group.add_command(create_share_cli, name='create-share')
+unity_catalog_group.add_command(list_shares_cli, name='list-shares')
+unity_catalog_group.add_command(get_share_cli, name='get-share')
+unity_catalog_group.add_command(update_share_cli, name='update-share')
+unity_catalog_group.add_command(delete_share_cli, name='delete-share')
+# Recipient cmds:
+unity_catalog_group.add_command(create_recipient_cli, name='create-recipient')
+unity_catalog_group.add_command(list_recipients_cli, name='list-recipients')
+unity_catalog_group.add_command(get_recipient_cli, name='get-recipient')
+unity_catalog_group.add_command(rotate_recipient_token_cli, name='rotate-recipient-token')
+unity_catalog_group.add_command(get_recipient_share_permissions_cli,
+                                name='get-recipient-share-perms')
+unity_catalog_group.add_command(delete_recipient_cli, name='delete-recipient')
+# Provider cmds:
+unity_catalog_group.add_command(create_provider_cli, name='create-provider')
+unity_catalog_group.add_command(list_providers_cli, name='list-providers')
+unity_catalog_group.add_command(get_provider_cli, name='get-provider')
+unity_catalog_group.add_command(update_provider_cli, name='update-provider')
+unity_catalog_group.add_command(delete_provider_cli, name='delete-provider')
+unity_catalog_group.add_command(list_provider_shares_cli, name='list-provider-shares')
diff --git a/databricks_cli/utils.py b/databricks_cli/utils.py
index f92a351c..7d0d6479 100644
--- a/databricks_cli/utils.py
+++ b/databricks_cli/utils.py
@@ -95,11 +95,13 @@ def error_and_quit(message):
     sys.exit(1)
 
 
-def pretty_format(json):
+def pretty_format(json, encode_utf8=False):
+    if encode_utf8:
+        return json_dumps(json, indent=2, ensure_ascii=False)
     return json_dumps(json, indent=2)
 
 
-def json_cli_base(json_file, json, api, print_response=True):
+def json_cli_base(json_file, json, api, print_response=True, encode_utf8=False):
     """
     Takes json_file or json string and calls an function "api" with the json
     deserialized
@@ -111,7 +113,7 @@ def json_cli_base(json_file, json, api, print_response=True):
             json = f.read()
     res = api(json_loads(json))
     if print_response:
-        click.echo(pretty_format(res))
+        click.echo(pretty_format(res, encode_utf8))
 
 
 def truncate_string(s, length=100):
diff --git a/examples/unity-catalog/create-cred1.json b/examples/unity-catalog/create-cred1.json
new file mode 100644
index 00000000..d2dd9cba
--- /dev/null
+++ b/examples/unity-catalog/create-cred1.json
@@ -0,0 +1,6 @@
+{
+    "name": "mycred",
+    "aws_iam_role": {
+        "role_arn": "arn:aws:iam::1234567890:role/MyRole-AJJHDSKSDF"
+    }
+}
diff --git a/examples/unity-catalog/create-dac1.json b/examples/unity-catalog/create-dac1.json
new file mode 100644
index 00000000..05ced568
--- /dev/null
+++ b/examples/unity-catalog/create-dac1.json
@@ -0,0 +1,6 @@
+{
+    "name": "My_DAC",
+    "aws_iam_role": {
+        "role_arn": "arn:aws:iam::1234567890:role/MyRole-AJJHDSKSDF"
+    }
+}
diff --git a/examples/unity-catalog/create-ext-table1.json b/examples/unity-catalog/create-ext-table1.json
new file mode 100644
index 00000000..0f61787e
--- /dev/null
+++ b/examples/unity-catalog/create-ext-table1.json
@@ -0,0 +1,27 @@
+{
+	"catalog_name": "main",
+	"schema_name": "default",
+	"name": "CHANGE_ME",
+	"comment": "This a CSV table in UC, created in API endpoint",
+	"table_type": "EXTERNAL",
+	"data_source_format": "CSV",
+	"columns": [
+		{
+			"name": "a",
+			"type_name": "INT",
+			"type_text": "int",
+			"type_json": "{\"name\":\"a\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}",
+			"nullable": true,
+			"position": 0
+		},
+		{
+			"name": "b",
+			"type_name": "STRING",
+			"type_text": "string",
+			"type_json": "{\"name\":\"b\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}",
+			"nullable": true,
+			"position": 1
+		}
+	],
+	"storage_location": "s3://us-west-2-extstaging-managed-catalog-test-bucket-2/my_test_uc_folder/"
+}
diff --git a/examples/unity-catalog/create-loc1.json b/examples/unity-catalog/create-loc1.json
new file mode 100644
index 00000000..532424ca
--- /dev/null
+++ b/examples/unity-catalog/create-loc1.json
@@ -0,0 +1,6 @@
+{
+    "name": "myloc1",
+    "credential_name": "mycred",
+    "comment": "hey hey",
+    "url": "s3://my.bucket"
+}
diff --git a/examples/unity-catalog/create-table1.json b/examples/unity-catalog/create-table1.json
new file mode 100644
index 00000000..c5f0dbf3
--- /dev/null
+++ b/examples/unity-catalog/create-table1.json
@@ -0,0 +1,38 @@
+{
+	"catalog_name": "main",
+	"schema_name": "default",
+	"name": "my_table",
+	"comment": "something really insightful",
+	"table_type": "MANAGED",
+	"data_source_format": "DELTA",
+	"columns": [
+		{
+			"name": "cust_id",
+			"type_name": "INT",
+			"type_text": "INT",
+			"type_json": "{\"name\":\"cust_id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}",
+			"nullable": false,
+			"position": 0
+		},
+		{
+			"name": "name",
+			"type_name": "STRING",
+			"type_text": "STRING",
+			"type_json": "{\"name\":\"name\",\"type\":\"string\",\"nullable\":false,\"metadata\":{}}",
+			"comment": "Short name",
+			"position": 1
+		},
+		{
+			"name": "some_flag",
+			"type_name": "BOOLEAN",
+			"type_text": "BOOLEAN",
+			"type_json": "{\"name\":\"some_flag\",\"type\":\"boolean\",\"nullable\":false,\"metadata\":{}}",
+			"nullable": false,
+			"position": 2
+		}
+	],
+	"properties": {
+		"first_key": "first value",
+		"second_key": "second value"
+	}
+}
diff --git a/examples/unity-catalog/create-view1.json b/examples/unity-catalog/create-view1.json
new file mode 100644
index 00000000..2a64fb80
--- /dev/null
+++ b/examples/unity-catalog/create-view1.json
@@ -0,0 +1,7 @@
+{
+	"catalog_name": "default",
+	"schema_name": "default",
+	"name": "my_view",
+	"table_type": "VIEW",
+	"view_definition": "CREATE VIEW AS SELECT * FROM some_table"
+}
diff --git a/examples/unity-catalog/replace-permissions1.json b/examples/unity-catalog/replace-permissions1.json
new file mode 100644
index 00000000..4268031f
--- /dev/null
+++ b/examples/unity-catalog/replace-permissions1.json
@@ -0,0 +1,18 @@
+{
+  "privilege_assignments": [
+    {
+      "privileges": [
+        "SELECT",
+        "USAGE"
+      ],
+      "principal": "foo@bar.com"
+    },
+    {
+      "privileges": [
+        "OWN",
+        "MODIFY"
+      ],
+      "principal": "some_group"
+    }
+  ]
+}
diff --git a/examples/unity-catalog/update-catalog.json b/examples/unity-catalog/update-catalog.json
new file mode 100644
index 00000000..ede80465
--- /dev/null
+++ b/examples/unity-catalog/update-catalog.json
@@ -0,0 +1,4 @@
+{
+  "name": "my_catalog",
+  "comment": "updated comment"
+}
diff --git a/examples/unity-catalog/update-metastore.json b/examples/unity-catalog/update-metastore.json
new file mode 100644
index 00000000..01878525
--- /dev/null
+++ b/examples/unity-catalog/update-metastore.json
@@ -0,0 +1,4 @@
+{
+  "name": "my_metastore",
+  "storage_root": "s3://my.other.bucket"
+}
diff --git a/examples/unity-catalog/update-permissions1.json b/examples/unity-catalog/update-permissions1.json
new file mode 100644
index 00000000..35b04201
--- /dev/null
+++ b/examples/unity-catalog/update-permissions1.json
@@ -0,0 +1,17 @@
+{
+  "changes": [
+    {
+      "principal": "adam.cain@databricks.com",
+      "add": ["SELECT"],
+      "remove": ["MODIFY"]
+    },
+    {
+      "principal": "eng-data-security",
+      "remove": ["CREATE"]
+    },
+    {
+      "principal": "users",
+      "add": ["USAGE"]
+    }
+  ]
+} 
diff --git a/examples/unity-catalog/update-schema.json b/examples/unity-catalog/update-schema.json
new file mode 100644
index 00000000..f43cf565
--- /dev/null
+++ b/examples/unity-catalog/update-schema.json
@@ -0,0 +1,4 @@
+{
+  "name": "my_schema",
+  "comment": "updated comment"
+}
diff --git a/examples/unity-catalog/update-table.json b/examples/unity-catalog/update-table.json
new file mode 100644
index 00000000..caa2b82d
--- /dev/null
+++ b/examples/unity-catalog/update-table.json
@@ -0,0 +1,4 @@
+{
+  "name": "my_table",
+  "comment": "updated comment"
+}

From 8c6266228a388af6f5dcd64f7c21cb69faa3581e Mon Sep 17 00:00:00 2001
From: Adam Cain <adam.cain@databricks.com>
Date: Fri, 19 Nov 2021 17:59:20 -0800
Subject: [PATCH 2/3] Unity Catalog changes from databricks-cli-private

* pylint fixes, version update
* Set version to '0.16.3.dev0.uc-int.1' (during previous master merge)
* Fix line-too-long errors from pylint.
* Split UC CLI commands (and API handlers, service class) into
  public and internal variants.
* Create mkdist.sh script for preparing distributions.
* Renamed 'get-recipient-share-perms' CLI as 'list-recipient-permissions'.
* Renamed 'list-tables-bulk' CLI as 'list-table-summaries'.
* Use 'list-share-permissions --name foo' and 'update-share-permissions --name foo'
  instead of 'get-permissions --share <foo>' and
  'update-permissions --share foo".
* Use 'hive_metastore' instead of 'main' as default-catalog-name in
  assign-metastore CLI.
* Support creating delta sharing catalog
* Fixed syntax, lint glitches
* Fix create-provider and update-provider namings
* Go back to using json_inline for UpdateExternalLocation (including
  the 'force' arg).
* Lint fixes.
* Unsplit the distribution (no more _internal.py src files or
  -int tarballs). Just create one UC distribution with all UC CLIs.
* For modularity, slit up the CLIs into metastore, catalog, etc.
  src files.
* Rename sharing -> delta_sharing
* delta-sharing improvements for SC-89547
* Use correct format
* Add 'unassign-metastore' command.
* Fix wording for 'assign-metastore' comments and help.
* mkdist script fixes
* Add new command to CLI
---
 databricks_cli/unity_catalog/api.py           |  151 +-
 databricks_cli/unity_catalog/catalog_cli.py   |  145 ++
 databricks_cli/unity_catalog/cli.py           | 1480 +----------------
 databricks_cli/unity_catalog/cred_cli.py      |  247 +++
 .../unity_catalog/delta_sharing_cli.py        |  484 ++++++
 databricks_cli/unity_catalog/ext_loc_cli.py   |  165 ++
 databricks_cli/unity_catalog/metastore_cli.py |  218 +++
 databricks_cli/unity_catalog/perms_cli.py     |  134 ++
 databricks_cli/unity_catalog/schema_cli.py    |  146 ++
 databricks_cli/unity_catalog/table_cli.py     |  170 ++
 databricks_cli/unity_catalog/uc_service.py    |  448 +++++
 databricks_cli/unity_catalog/utils.py         |   29 +
 tools/mkdist.sh                               |   50 +
 tox.ini                                       |    2 +-
 14 files changed, 2335 insertions(+), 1534 deletions(-)
 create mode 100644 databricks_cli/unity_catalog/catalog_cli.py
 create mode 100644 databricks_cli/unity_catalog/cred_cli.py
 create mode 100644 databricks_cli/unity_catalog/delta_sharing_cli.py
 create mode 100644 databricks_cli/unity_catalog/ext_loc_cli.py
 create mode 100644 databricks_cli/unity_catalog/metastore_cli.py
 create mode 100644 databricks_cli/unity_catalog/perms_cli.py
 create mode 100644 databricks_cli/unity_catalog/schema_cli.py
 create mode 100644 databricks_cli/unity_catalog/table_cli.py
 create mode 100644 databricks_cli/unity_catalog/uc_service.py
 create mode 100644 databricks_cli/unity_catalog/utils.py
 create mode 100755 tools/mkdist.sh

diff --git a/databricks_cli/unity_catalog/api.py b/databricks_cli/unity_catalog/api.py
index 589531ad..ef204f67 100644
--- a/databricks_cli/unity_catalog/api.py
+++ b/databricks_cli/unity_catalog/api.py
@@ -1,5 +1,5 @@
 # Databricks CLI
-# Copyright 2017 Databricks, Inc.
+# Copyright 2021 Databricks, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License"), except
 # that the use of services to which certain application programming
@@ -20,7 +20,8 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-from databricks_cli.sdk import UnityCatalogService
+
+from databricks_cli.unity_catalog.uc_service import UnityCatalogService
 
 
 class UnityCatalogApi(object):
@@ -55,10 +56,61 @@ def update_metastore_assignment(self, workspace_id, metastore_id, default_catalo
         return self.client.update_metastore_assignment(workspace_id, metastore_id,
                                                        default_catalog_name)
 
+    def delete_metastore_assignment(self, workspace_id, metastore_id):
+        return self.client.delete_metastore_assignment(workspace_id, metastore_id)
+
+    # External Location APIs
+
+    def create_external_location(self, loc_spec):
+        return self.client.create_external_location(loc_spec)
+
+    def list_external_locations(self):
+        return self.client.list_external_locations()
+
+    def get_external_location(self, name):
+        return self.client.get_external_location(name)
+
+    def update_external_location(self, name, force, loc_spec):
+        return self.client.update_external_location(name, force, loc_spec)
+
+    def delete_external_location(self, name, force):
+        return self.client.delete_external_location(name, force)
+
+    # Data Access Configuration APIs
+
+    def create_dac(self, metastore_id, dac):
+        return self.client.create_dac(metastore_id, dac)
+
+    def list_dacs(self, metastore_id):
+        return self.client.list_dacs(metastore_id)
+
+    def get_dac(self, metastore_id, dac_id):
+        return self.client.get_dac(metastore_id, dac_id)
+
+    def delete_dac(self, metastore_id, dac_id):
+        return self.client.delete_dac(metastore_id, dac_id)
+
+    # Storage Credentials
+
+    def create_storage_credential(self, cred_spec):
+        return self.client.create_storage_credential(cred_spec)
+
+    def list_storage_credentials(self, name_pattern):
+        return self.client.list_storage_credentials(name_pattern)
+
+    def get_storage_credential(self, name):
+        return self.client.get_storage_credential(name)
+
+    def update_storage_credential(self, name, cred_spec):
+        return self.client.update_storage_credential(name, cred_spec)
+
+    def delete_storage_credential(self, name):
+        return self.client.delete_storage_credential(name)
+
     # Catalog APIs
 
-    def create_catalog(self, catalog_name, comment):
-        return self.client.create_catalog(catalog_name, comment)
+    def create_catalog(self, catalog_name, comment, provider, share):
+        return self.client.create_catalog(catalog_name, comment, provider, share)
 
     def list_catalogs(self):
         return self.client.list_catalogs()
@@ -97,8 +149,8 @@ def create_table(self, table_spec):
     def list_tables(self, catalog_name, schema_name, name_pattern):
         return self.client.list_tables(catalog_name, schema_name, name_pattern)
 
-    def list_tables_bulk(self, catalog_name):
-        return self.client.list_tables_bulk(catalog_name)
+    def list_table_summaries(self, catalog_name):
+        return self.client.list_table_summaries(catalog_name)
 
     def get_table(self, full_name):
         return self.client.get_table(full_name)
@@ -109,68 +161,6 @@ def update_table(self, full_name, table_spec):
     def delete_table(self, table_full_name):
         return self.client.delete_table(table_full_name)
 
-    # Data Access Configuration APIs
-
-    def create_dac(self, metastore_id, dac):
-        return self.client.create_dac(metastore_id, dac)
-
-    def list_dacs(self, metastore_id):
-        return self.client.list_dacs(metastore_id)
-
-    def get_dac(self, metastore_id, dac_id):
-        return self.client.get_dac(metastore_id, dac_id)
-
-    def delete_dac(self, metastore_id, dac_id):
-        return self.client.delete_dac(metastore_id, dac_id)
-
-    def create_root_credentials(self, root_creds):
-        return self.client.create_root_credentials(root_creds)
-
-    # Storage Credentials
-
-    def create_storage_credential(self, cred_spec):
-        return self.client.create_storage_credential(cred_spec)
-
-    def list_storage_credentials(self, name_pattern):
-        return self.client.list_storage_credentials(name_pattern)
-
-    def get_storage_credential(self, name):
-        return self.client.get_storage_credential(name)
-
-    def update_storage_credential(self, name, cred_spec):
-        return self.client.update_storage_credential(name, cred_spec)
-
-    def delete_storage_credential(self, name):
-        return self.client.delete_storage_credential(name)
-
-    # External Locations
-
-    def create_external_location(self, loc_spec):
-        return self.client.create_external_location(loc_spec)
-
-    def list_external_locations(self):
-        return self.client.list_external_locations()
-
-    def get_external_location(self, name):
-        return self.client.get_external_location(name)
-
-    def update_external_location(self, name, force, loc_spec):
-        return self.client.update_external_location(name, force, loc_spec)
-
-    def delete_external_location(self, name, force):
-        return self.client.delete_external_location(name, force)
-
-    # Permissions APIs
-
-    def get_permissions(self, sec_type, sec_name):
-        return self.client.get_permissions(sec_type, sec_name)
-
-    def update_permissions(self, sec_type, sec_name, diff_spec):
-        return self.client.update_permissions(sec_type, sec_name, diff_spec)
-
-    def replace_permissions(self, sec_type, sec_name, perm_spec):
-        return self.client.replace_permissions(sec_type, sec_name, perm_spec)
-
     # Share APIs
 
     def create_share(self, name):
@@ -188,6 +178,12 @@ def update_share(self, name, share_spec):
     def delete_share(self, name):
         return self.client.delete_share(name)
 
+    def list_share_permissions(self, name):
+        return self.client.list_share_permissions(name)
+
+    def update_share_permissions(self, name, perm_spec):
+        return self.client.update_share_permissions(name, perm_spec)
+
     # Recipient APIs
 
     def create_recipient(self, name, comment):
@@ -210,8 +206,8 @@ def delete_recipient(self, name):
 
     # Provider APIs
 
-    def create_provider(self, name, comment, provider_profile):
-        return self.client.create_provider(name, comment, provider_profile)
+    def create_provider(self, name, comment, recipient_profile):
+        return self.client.create_provider(name, comment, recipient_profile)
 
     def list_providers(self):
         return self.client.list_providers()
@@ -219,11 +215,22 @@ def list_providers(self):
     def get_provider(self, name):
         return self.client.get_provider(name)
 
-    def update_provider(self, name, new_name=None, comment=None, provider_profile=None):
-        return self.client.update_provider(name, new_name, comment, provider_profile)
+    def update_provider(self, name, new_name=None, comment=None, recipient_profile=None):
+        return self.client.update_provider(name, new_name, comment, recipient_profile)
 
     def delete_provider(self, name):
         return self.client.delete_provider(name)
 
     def list_provider_shares(self, name):
         return self.client.list_provider_shares(name)
+
+    # Permissions APIs
+
+    def get_permissions(self, sec_type, sec_name):
+        return self.client.get_permissions(sec_type, sec_name)
+
+    def update_permissions(self, sec_type, sec_name, diff_spec):
+        return self.client.update_permissions(sec_type, sec_name, diff_spec)
+
+    def replace_permissions(self, sec_type, sec_name, perm_spec):
+        return self.client.replace_permissions(sec_type, sec_name, perm_spec)
diff --git a/databricks_cli/unity_catalog/catalog_cli.py b/databricks_cli/unity_catalog/catalog_cli.py
new file mode 100644
index 00000000..3254823a
--- /dev/null
+++ b/databricks_cli/unity_catalog/catalog_cli.py
@@ -0,0 +1,145 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import JsonClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new catalog.')
+@click.option('--name', required=True, help='Name of new catalog.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@click.option('--provider', default=None, required=False,
+              help='Name of the Provider (for creating Delta Sharing Catalog).')
+@click.option('--share', default=None, required=False,
+              help='Name of the Share under the Provider to create a Delta Sharing Catalog.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_catalog_cli(api_client, name, comment, provider, share):
+    """
+    Create a new catalog.
+
+    Calls the 'createCatalog' RPC endpoint of the Unity Catalog service.
+    Returns the CatalogInfo for the newly-created catalog.
+
+    """
+    catalog_json = UnityCatalogApi(api_client).create_catalog(name, comment,
+                                                              provider, share)
+    click.echo(mc_pretty_format(catalog_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List catalogs.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_catalogs_cli(api_client):
+    """
+    List catalogs.
+
+    Calls the 'listCatalogs' RPC endpoint of the Unity Catalog service.
+    Returns array of CatalogInfos.
+
+    """
+    catalogs_json = UnityCatalogApi(api_client).list_catalogs()
+    click.echo(mc_pretty_format(catalogs_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a catalog.')
+@click.option('--name', required=True,
+              help='Name of the catalog to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_catalog_cli(api_client, name):
+    """
+    Get a catalog.
+
+    Calls the 'getCatalog' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    catalog_json = UnityCatalogApi(api_client).get_catalog(name)
+    click.echo(mc_pretty_format(catalog_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a catalog.')
+@click.option('--name', required=True,
+              help='Name of the catalog to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/catalogs'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_catalog_cli(api_client, name, json_file, json):
+    """
+    Update a catalog.
+
+    Calls the 'updateCatalog' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_catalog(name, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a catalog.')
+@click.option('--name', required=True,
+              help='Name of the catalog to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_catalog_cli(api_client, name):
+    """
+    Delete a catalog.
+
+    Calls the 'deleteCatalog' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_catalog(name)
+
+
+def register_catalog_commands(cmd_group):
+    cmd_group.add_command(create_catalog_cli, name='create-catalog')
+    cmd_group.add_command(list_catalogs_cli, name='list-catalogs')
+    cmd_group.add_command(get_catalog_cli, name='get-catalog')
+    cmd_group.add_command(update_catalog_cli, name='update-catalog')
+    cmd_group.add_command(delete_catalog_cli, name='delete-catalog')
diff --git a/databricks_cli/unity_catalog/cli.py b/databricks_cli/unity_catalog/cli.py
index 2c667245..61490983 100644
--- a/databricks_cli/unity_catalog/cli.py
+++ b/databricks_cli/unity_catalog/cli.py
@@ -1,5 +1,5 @@
 # Databricks CLI
-# Copyright 2017 Databricks, Inc.
+# Copyright 2021 Databricks, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License"), except
 # that the use of services to which certain application programming
@@ -23,1397 +23,18 @@
 
 import click
 
-from databricks_cli.click_types import MetastoreIdClickType, DacIdClickType, \
-    WorkspaceIdClickType, JsonClickType, OneOfOption
-from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
-from databricks_cli.unity_catalog.api import UnityCatalogApi
-from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, pretty_format, json_cli_base
+from databricks_cli.configure.config import profile_option, debug_option
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS
 from databricks_cli.version import print_version_callback, version
 
-
-# Encode UTF-8 strings in JSON blobs
-def mc_pretty_format(json):
-    return pretty_format(json, encode_utf8=True)
-
-
-#################  Metastore Commands  #####################
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a metastore.')
-@click.option('--name', required=True, help='Name of the new metastore.')
-@click.option('--storage-root', required=True,
-              help='Storage root URL for the new metastore.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_metastore_cli(api_client, name, storage_root):
-    """
-    Create new metastore specified by the JSON input.
-
-    Calls the 'createMetastore' RPC endpoint of the Unity Catalog service.
-    Returns the properties of the newly-created metastore.
-
-    """
-    metastore_json = UnityCatalogApi(api_client).create_metastore(name, storage_root)
-    click.echo(mc_pretty_format(metastore_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List metastores.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_metastores_cli(api_client):
-    """
-    List metastores.
-
-    Calls the 'listMetastores' RPC endpoint of the Unity Catalog service.
-    Returns array of MetastoreInfos.
-
-    """
-    metastores_json = UnityCatalogApi(api_client).list_metastores()
-    click.echo(mc_pretty_format(metastores_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a metastore.')
-@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_metastore_cli(api_client, metastore_id):
-    """
-    Get a metastore.
-
-    Calls the 'getMetastore' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    metastore_json = UnityCatalogApi(api_client).get_metastore(metastore_id)
-    click.echo(mc_pretty_format(metastore_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a metastore.')
-@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore to update.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/admin/metastores'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_metastore_cli(api_client, metastore_id, json_file, json):
-    """
-    Update a metastore.
-
-    Calls the 'updateMetastore' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_metastore(metastore_id, json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a metastore.')
-@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore to delete.')
-@click.option('--force', '-f', is_flag=True, default=False)
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_metastore_cli(api_client, metastore_id, force):
-    """
-    Delete a metastore.
-
-    Calls the 'deleteMetastore' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_metastore(metastore_id, force)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get summary info of current metastore.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def metastore_summary_cli(api_client):
-    """
-    Get metastore summary.
-
-    Calls the 'getMetastoreSummary' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    summary_json = UnityCatalogApi(api_client).get_metastore_summary()
-    click.echo(mc_pretty_format(summary_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Assign a metastore to the current workspace.')
-@click.option('--workspace-id', 'workspace_id', required=True, type=WorkspaceIdClickType(),
-              help='Unique identifier of the metastore to assign.')
-@click.option('--metastore-id', 'metastore_id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore to assign.')
-@click.option('--default-catalog-name', 'default_catalog_name', required=False, default='main',
-              help='Name of the default catalog to use with the metastore.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def assign_metastore_cli(api_client, workspace_id, metastore_id, default_catalog_name):
-    """
-    Assign a metastore to the current workspace.
-
-    Calls the 'createMetastoreAssignment' RPC endpoint of the Unity Catalog service.
-    If that fails due to the workspace already having a Metastore assigned, it calls
-    the 'updateMetastoreAssignment' endpoint.
-    Returns nothing.
-
-    """
-    resp = UnityCatalogApi(api_client).create_metastore_assignment(workspace_id, metastore_id,
-                                                                   default_catalog_name)
-    # resp will just be an empty object ('{}') but it's good to print *something*
-    click.echo(mc_pretty_format(resp))
-
-
-##############  Catalog Commands  ##############
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a new catalog.')
-@click.option('--name', required=True, help='Name of new catalog.')
-@click.option('--comment', default=None, required=False,
-              help='Free-form text description.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_catalog_cli(api_client, name, comment):
-    """
-    Create a new catalog.
-
-    Calls the 'createCatalog' RPC endpoint of the Unity Catalog service.
-    Returns the CatalogInfo for the newly-created catalog.
-
-    """
-    catalog_json = UnityCatalogApi(api_client).create_catalog(name, comment)
-    click.echo(mc_pretty_format(catalog_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List catalogs.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_catalogs_cli(api_client):
-    """
-    List catalogs.
-
-    Calls the 'listCatalogs' RPC endpoint of the Unity Catalog service.
-    Returns array of CatalogInfos.
-
-    """
-    catalogs_json = UnityCatalogApi(api_client).list_catalogs()
-    click.echo(mc_pretty_format(catalogs_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a catalog.')
-@click.option('--name', required=True,
-              help='Name of the catalog to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_catalog_cli(api_client, name):
-    """
-    Get a catalog.
-
-    Calls the 'getCatalog' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    catalog_json = UnityCatalogApi(api_client).get_catalog(name)
-    click.echo(mc_pretty_format(catalog_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a catalog.')
-@click.option('--name', required=True,
-              help='Name of the catalog to update.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/catalogs'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_catalog_cli(api_client, name, json_file, json):
-    """
-    Update a catalog.
-
-    Calls the 'updateCatalog' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_catalog(name, json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a catalog.')
-@click.option('--name', required=True,
-              help='Name of the catalog to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_catalog_cli(api_client, name):
-    """
-    Delete a catalog.
-
-    Calls the 'deleteCatalog' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_catalog(name)
-
-
-#############  Schema Commands ##############
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a new schema.')
-@click.option('--catalog-name', required=True, help='Parent catalog of new schema.')
-@click.option('--name', required=True,
-              help='Name of new schema, relative to parent catalog.')
-@click.option('--comment', default=None, required=False,
-              help='Free-form text description.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_schema_cli(api_client, catalog_name, name, comment):
-    """
-    Create a new schema in the specified catalog.
-
-    Calls the 'createSchema' RPC endpoint of the Unity Catalog service.
-    Returns the SchemaInfo for the newly-created schema.
-
-    """
-    schema_json = UnityCatalogApi(api_client).create_schema(catalog_name, name, comment)
-    click.echo(mc_pretty_format(schema_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List schemas.')
-@click.option('--catalog-name', required=True,
-              help='Name of the parent catalog for schemas of interest.')
-@click.option('--name-pattern', default=None,
-              help='SQL LIKE pattern that the schema name must match to be in list.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_schemas_cli(api_client, catalog_name, name_pattern):
-    """
-    List schemas.
-
-    Calls the 'listSchemas' RPC endpoint of the Unity Catalog service.
-    Returns array of SchemaInfos.
-
-    """
-    schemas_json = UnityCatalogApi(api_client).list_schemas(catalog_name, name_pattern)
-    click.echo(mc_pretty_format(schemas_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a schema.')
-@click.option('--full-name', required=True,
-              help='Full name (<catalog>.<schema>) of the schema to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_schema_cli(api_client, full_name):
-    """
-    Get a schema.
-
-    Calls the 'getSchema' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    schema_json = UnityCatalogApi(api_client).get_schema(full_name)
-    click.echo(mc_pretty_format(schema_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a schema.')
-@click.option('--full-name', required=True,
-              help='Full name (<catalog>.<schema>) of the schema to update.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/schemas'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_schema_cli(api_client, full_name, json_file, json):
-    """
-    Update a schema.
-
-    Calls the 'updateSchema' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_schema(full_name, json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a schema.')
-@click.option('--full-name', required=True,
-              help='Full name (<catalog>.<schema>) of the schema to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_schema_cli(api_client, full_name):
-    """
-    Delete a schema.
-
-    Calls the 'deleteSchema' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_schema(full_name)
-
-
-##############  Table Commands  #################
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a table.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to POST.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/tables'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_table_cli(api_client, json_file, json):
-    """
-    Create new table specified by the JSON input.
-
-    Calls the 'createTable' RPC endpoint of the Unity Catalog service.
-    Returns the properties of the newly-created table.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).create_table(json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List tables.')
-@click.option('--catalog-name', required=True,
-              help='Name of the parent catalog for tables of interest.')
-@click.option('--schema-name', required=True,
-              help='Name of the parent schema for tables of interest.')
-@click.option('--name-pattern', default=None,
-              help='SQL LIKE pattern that the table name must match to be in list.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_tables_cli(api_client, catalog_name, schema_name, name_pattern):
-    """
-    List tables.
-
-    Calls the 'listTables' RPC endpoint of the Unity Catalog service.
-    Returns array of TableInfos.
-
-    """
-    tables_json = UnityCatalogApi(api_client).list_tables(catalog_name, schema_name, name_pattern)
-    click.echo(mc_pretty_format(tables_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List tables bulk.')
-@click.option('--catalog-name', required=True,
-              help='Name of the parent catalog for tables of interest.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_tables_bulk_cli(api_client, catalog_name):
-    """
-    List tables bulk.
-
-    Calls the 'listTablesBulk' RPC endpoint of the Unity Catalog service.
-    Returns array of TableSummarys.
-
-    """
-    tables_json = UnityCatalogApi(api_client).list_tables_bulk(catalog_name)
-    click.echo(mc_pretty_format(tables_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a table.')
-@click.option('--full-name', required=True,
-              help='Full name (<catalog>.<schema>.<table>) of the table to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_table_cli(api_client, full_name):
-    """
-    Get a table.
-
-    Calls the 'getTable' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    table_json = UnityCatalogApi(api_client).get_table(full_name)
-    click.echo(mc_pretty_format(table_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a table.')
-@click.option('--full-name', required=True,
-              help='Full name (<catalog>.<schema>.<table>) of the table to update.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/tables'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_table_cli(api_client, full_name, json_file, json):
-    """
-    Update a table.
-
-    Calls the 'updateTable' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_table(full_name, json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a table.')
-@click.option('--full-name', required=True,
-              help='Full name (<catalog>.<schema>.<table>) of the table to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_table_cli(api_client, full_name):
-    """
-    Delete a table.
-
-    Calls the 'deleteTable' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_table(full_name)
-
-
-#############  Data Access Configuration Commands  ############
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create data access configuration.')
-@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore parent of the DAC.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to POST.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/data-access-configurations'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_dac_cli(api_client, metastore_id, json_file, json):
-    """
-    Create new data access configuration.
-
-    Calls the 'createDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
-    The specification for the request json can be found at <insert doc link here>.
-    Returns the properties of the newly-created DAC.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).create_dac(metastore_id, json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List data access configurations.')
-@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore parent of the DAC(s).')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_dacs_cli(api_client, metastore_id):
-    """
-    List data access configurations.
-
-    Calls the 'listDataAccessConfigurations' RPC endpoint of the Unity Catalog service.
-    Returns array of DataAccessConfigurations.
-
-    """
-    dacs_json = UnityCatalogApi(api_client).list_dacs(metastore_id)
-    click.echo(mc_pretty_format(dacs_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get data access configuration.')
-@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore parent of the DAC.')
-@click.option('--dac-id', required=True, type=DacIdClickType(),
-              help='Data access configuration ID.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_dac_cli(api_client, metastore_id, dac_id):
-    """
-    Get data access configuration details.
-
-    Calls the 'getDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
-    Returns details of the DAC specified by its id (TODO: lookup by DAC name?).
-
-    """
-    dac_json = UnityCatalogApi(api_client).get_dac(metastore_id, dac_id)
-    click.echo(mc_pretty_format(dac_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete data access configuration.')
-@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
-              help='Unique identifier of the metastore parent of the DAC.')
-@click.option('--dac-id', required=True, type=DacIdClickType(),
-              help='Data access configuration ID.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_dac_cli(api_client, metastore_id, dac_id):
-    """
-    Delete data access configuration details.
-
-    Calls the 'deleteDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
-
-    """
-    UnityCatalogApi(api_client).delete_dac(metastore_id, dac_id)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create temporary credentials for storage root access.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to POST.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/root-credentials'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_root_credentials_cli(api_client, json_file, json):
-    """
-    Create new temporary credentials (token) for storage root access.
-
-    Calls the 'createRootCredentials' RPC endpoint of the Unity Catalog service.
-    The specification for the request json can be found at <insert doc link here>.
-    Returns the newly-created temporary credentials.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).create_root_credentials(json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create storage credential.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to POST.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/storage-credentials'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_credential_cli(api_client, json_file, json):
-    """
-    Create new storage credential.
-
-    Calls the 'createStorageCredential' RPC endpoint of the Unity Catalog service.
-    The specification for the request json can be found at <insert doc link here>.
-    Returns the properties of the newly-created Storage Credential.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).create_storage_credential(json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List storage credentials.')
-@click.option('--name-pattern', default=None,
-              help='SQL LIKE pattern that the credential name must match to be in list.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_credentials_cli(api_client, name_pattern):
-    """
-    List storage credentials.
-
-    Calls the 'listStorageCredentials' RPC endpoint of the Unity Catalog service.
-    Returns array of StorageCredentials.
-
-    """
-    creds_json = UnityCatalogApi(api_client).list_storage_credentials(name_pattern)
-    click.echo(mc_pretty_format(creds_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a storage credential.')
-@click.option('--name', required=True,
-              help='Name of the storage credential to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_credential_cli(api_client, name):
-    """
-    Get a storage credential.
-
-    Calls the 'getStorageCredential' RPC endpoint of the Unity Catalog service.
-    Returns an StorageCredential object.
-
-    """
-    cred_json = UnityCatalogApi(api_client).get_storage_credential(name)
-    click.echo(mc_pretty_format(cred_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a storage credential.')
-@click.option('--name', required=True,
-              help='Name of the storage credential to update.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/storage-credentials'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_credential_cli(api_client, name, json_file, json):
-    """
-    Update a storage credential.
-
-    Calls the 'updateStorageCredential' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_storage_credential(name, json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a storage credential.')
-@click.option('--name', required=True,
-              help='Name of the storage credential to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_credential_cli(api_client, name):
-    """
-    Delete a storage credential.
-
-    Calls the 'deleteStorageCredential' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_storage_credential(name)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create External Location.')
-@click.option('--name', default=None,
-              help='Name of new external location')
-@click.option('--url', default=None,
-              help='Path URL for the new external location')
-@click.option('--storage-credential-name', default=None,
-              help='Name of storage credential to use with new external location')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to POST.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/external-locations'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_location_cli(api_client, name, url, storage_credential_name, json_file, json):
-    """
-    Create new external location.
-
-    Calls the 'createExternalLocation' RPC endpoint of the Unity Catalog service.
-    The specification for the request json can be found at <insert doc link here>.
-    Returns the properties of the newly-created Storage Credential.
-
-    """
-    if (name is not None) and (url is not None) and (storage_credential_name is not None):
-        if (json_file is not None) or (json is not None):
-            raise ValueError('Cannot specify JSON if both name and url are given')
-        data = {"name": name, "url": url, "credential_name": storage_credential_name}
-        loc_json = UnityCatalogApi(api_client).create_external_location(data)
-        click.echo(mc_pretty_format(loc_json))
-    elif (json is None) and (json_file is None):
-        raise ValueError('Must provide name, url and storage-credential-name or use JSON specification')
-    else:
-        json_cli_base(json_file, json,
-                      lambda json: UnityCatalogApi(api_client).create_external_location(json),
-                      encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List external locations.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_locations_cli(api_client, ):
-    """
-    List external locations.
-
-    Calls the 'listExternalLocations' RPC endpoint of the Unity Catalog service.
-    Returns array of ExternalLocations.
-
-    """
-    locs_json = UnityCatalogApi(api_client).list_external_locations()
-    click.echo(mc_pretty_format(locs_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get an external location.')
-@click.option('--name', required=True,
-              help='Name of the external location to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_location_cli(api_client, name):
-    """
-    Get an external location.
-
-    Calls the 'getExternalLocation' RPC endpoint of the Unity Catalog service.
-    Returns an ExternalLocation object.
-
-    """
-    loc_json = UnityCatalogApi(api_client).get_external_location(name)
-    click.echo(mc_pretty_format(loc_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update an external location.')
-@click.option('--name', required=True,
-              help='Name of the external location to update.')
-@click.option('--force', '-f', is_flag=True, default=False,
-              help='Force update even if location has dependent tables/mounts')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/external-locations'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_location_cli(api_client, name, force, json_file, json):
-    """
-    Update an external location.
-
-    Calls the 'updateExternalLocation' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_external_location(name, force, json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete an external location.')
-@click.option('--name', required=True,
-              help='Name of the external location to delete.')
-@click.option('--force', '-f', is_flag=True, default=False,
-              help='Force deletion even if location has dependent tables/mounts')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_location_cli(api_client, name, force):
-    """
-    Delete an external location.
-
-    Calls the 'deleteExternalLocation' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_external_location(name, force)
-
-
-PERMISSIONS_OBJ_TYPES = [
-    'catalog', 'schema', 'table', 'share', 'storage-credential', 'external-location'
-]
-
-
-def _get_perm_securable_name_and_type(catalog_name, schema_full_name, table_full_name,
-                                      share_name, credential_name, location_name):
-    if catalog_name:
-        return ('catalog', catalog_name)
-    elif schema_full_name:
-        return ('schema', schema_full_name)
-    elif table_full_name:
-        return ('table', table_full_name)
-    elif share_name:
-        return ('share', share_name)
-    elif credential_name:
-        return ('storage-credential', credential_name)
-    else:
-        return ('external-location', location_name)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get permissions on a securable.')
-@click.option('--catalog', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of catalog of interest')
-@click.option('--schema', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Full name of schema of interest')
-@click.option('--table', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Full name of table of interest')
-@click.option('--share', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the share of interest')
-@click.option('--storage-credential', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the storage credential of interest')
-@click.option('--external-location', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the external location of interest')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_permissions_cli(api_client, catalog, schema, table, share, storage_credential,
-                        external_location):
-    """
-    Get permissions on a securable.
-
-    Calls the 'getPermissions' RPC endpoint of the Unity Catalog service.
-    Returns PermissionsList for the requested securable.
-
-    """
-    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table, share,
-                                                           storage_credential, external_location)
-
-    perm_json = UnityCatalogApi(api_client).get_permissions(sec_type, sec_name)
-    click.echo(mc_pretty_format(perm_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='update permissions on a securable.')
-@click.option('--catalog', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of catalog of interest')
-@click.option('--schema', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Full name of schema of interest')
-@click.option('--table', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Full name of table of interest')
-@click.option('--share', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the share of interest')
-@click.option('--storage-credential', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the storage credential of interest')
-@click.option('--external-location', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the external location of interest')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON of permissions change to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/permissions'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_permissions_cli(api_client, catalog, schema, table, share, storage_credential,
-                           external_location, json_file, json):
-    """
-    Update permissions on a securable.
-
-    Calls the 'updatePermissions' RPC endpoint of the Unity Catalog service.
-    Returns updated PermissionsList for the requested securable.
-
-    """
-    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table, share,
-                                                           storage_credential, external_location)
-
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).update_permissions(sec_type, sec_name,
-                                                                              json),
-                  encode_utf8=True)
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='replace permissions on a securable.')
-@click.option('--catalog', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of catalog of interest')
-@click.option('--schema', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Full name of schema of interest')
-@click.option('--table', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Full name of table of interest')
-# shares not supported for replace permissions
-@click.option('--credential', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the storage credential of interest')
-@click.option('--location', cls=OneOfOption, default=None,
-              one_of=PERMISSIONS_OBJ_TYPES,
-              help='Name of the external location of interest')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON of permissions to PUT.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/permissions'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def replace_permissions_cli(api_client, catalog, schema, table, credential, location,
-                            json_file, json):
-    """
-    Replace permissions on a securable.
-
-    Calls the 'replacePermissions' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    share = None  # shares not supported for replace permissions
-
-    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table, share,
-                                                           credential, location)
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).replace_permissions(sec_type, sec_name,
-                                                                               json),
-                  encode_utf8=True)
-
-##############  Share Commands  ##############
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a new share.')
-@click.option('--name', required=True, help='Name of new share.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_share_cli(api_client, name):
-    """
-    Create a new share.
-
-    Calls the 'createShare' RPC endpoint of the Unity Catalog service.
-    Returns the ShareInfo for the newly-created share.
-
-    """
-    share_json = UnityCatalogApi(api_client).create_share(name)
-    click.echo(mc_pretty_format(share_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List shares.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_shares_cli(api_client):
-    """
-    List shares.
-
-    Calls the 'listShares' RPC endpoint of the Unity Catalog service.
-    Returns array of ShareInfos.
-
-    """
-    shares_json = UnityCatalogApi(api_client).list_shares()
-    click.echo(mc_pretty_format(shares_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a share.')
-@click.option('--name', required=True,
-              help='Name of the share to get.')
-@click.option('--include-shared-data', default=True,
-              help='Whether to include shared data in the response.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_share_cli(api_client, name, include_shared_data):
-    """
-    Get a share.
-
-    Calls the 'getShare' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    share_json = UnityCatalogApi(api_client).get_share(name, include_shared_data)
-    click.echo(mc_pretty_format(share_json))
-
-
-def shared_data_object(name):
-    return {'name': name, 'data_object_type': 'TABLE'}
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a share.')
-@click.option('--name', required=True,
-              help='Name of the share to update.')
-@click.option('--add-table', default=None, multiple=True,
-              help='Full name of table to add to share')
-@click.option('--remove-table', default=None, multiple=True,
-              help='Full name of table to remove from share')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON request to PATCH.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/shares'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_share_cli(api_client, name, add_table, remove_table, json_file, json):
-    """
-    Update a share.
-
-    Calls the 'updateShare' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    if len(add_table) > 0 or len(remove_table) > 0:
-        updates = []
-        for a in add_table:
-            updates.append({'action': 'ADD', 'data_object': shared_data_object(a)})
-        for r in remove_table:
-            updates.append({'action': 'REMOVE', 'data_object': shared_data_object(r)})
-        d = {'updates': updates}
-        share_json = UnityCatalogApi(api_client).update_share(name, d)
-        click.echo(mc_pretty_format(share_json))
-    else:
-        json_cli_base(json_file, json,
-                      lambda json: UnityCatalogApi(api_client).update_share(name, json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a share.')
-@click.option('--name', required=True,
-              help='Name of the share to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_share_cli(api_client, name):
-    """
-    Delete a share.
-
-    Calls the 'deleteShare' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_share(name)
-
-
-##############  Recipient Commands  ##############
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a new recipient.')
-@click.option('--name', required=True, help='Name of new recipient.')
-@click.option('--comment', default=None, required=False,
-              help='Free-form text description.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_recipient_cli(api_client, name, comment):
-    """
-    Create a new recipient.
-
-    Calls the 'createRecipient' RPC endpoint of the Unity Catalog service.
-    Returns the RecipientInfo for the newly-created recipient.
-
-    """
-    recipient_json = UnityCatalogApi(api_client).create_recipient(name, comment)
-    click.echo(mc_pretty_format(recipient_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List recipients.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_recipients_cli(api_client):
-    """
-    List recipients.
-
-    Calls the 'listRecipients' RPC endpoint of the Unity Catalog service.
-    Returns array of RecipientInfos.
-
-    """
-    recipients_json = UnityCatalogApi(api_client).list_recipients()
-    click.echo(mc_pretty_format(recipients_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a recipient.')
-@click.option('--name', required=True,
-              help='Name of the recipient to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_recipient_cli(api_client, name):
-    """
-    Get a recipient.
-
-    Calls the 'getRecipient' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    recipient_json = UnityCatalogApi(api_client).get_recipient(name)
-    click.echo(mc_pretty_format(recipient_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Rotate token for the recipient.')
-@click.option('--name', required=True, help='Name of new recipient.')
-@click.option('--existing-token-expire-in-seconds', default=None, required=False,
-              help='Expire the existing token in number of seconds from now, 0 to expire it immediately.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def rotate_recipient_token_cli(api_client, name, existing_token_expire_in_seconds):
-    """
-    Rotate recipient token.
-
-    Calls the 'rotateRecipientToken' RPC endpoint of the Unity Catalog service.
-    Returns the RecipientInfo for the recipient with rotated tokens.
-
-    """
-    recipient_json = UnityCatalogApi(api_client).rotate_recipient_token(name, existing_token_expire_in_seconds)
-    click.echo(mc_pretty_format(recipient_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get share permissions of a recipient.')
-@click.option('--name', required=True,
-              help='Name of the recipient.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_recipient_share_permissions_cli(api_client, name):
-    """
-    Get a recipient's share permissions.
-
-    Calls the 'getRecipientSharePermissions' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    recipient_json = UnityCatalogApi(api_client).get_recipient_share_permissions(name)
-    click.echo(mc_pretty_format(recipient_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a recipient.')
-@click.option('--name', required=True,
-              help='Name of the recipient to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_recipient_cli(api_client, name):
-    """
-    Delete a recipient.
-
-    Calls the 'deleteRecipient' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_recipient(name)
-
-
-##############  Provider Commands  ##############
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Create a provider.')
-@click.option('--name', required=True, help='Name of the new provider.')
-@click.option('--comment', default=None, required=False,
-              help='Free-form text description.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON provider profile.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/providers'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def create_provider_cli(api_client, name, comment, json_file, json):
-    """
-    Create a provider.
-
-    Calls the 'createProvider' RPC endpoint of the Unity Catalog service.
-    Returns the created provider info.
-
-    """
-    json_cli_base(json_file, json,
-                  lambda json: UnityCatalogApi(api_client).create_provider(name, comment, json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List providers.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_providers_cli(api_client):
-    """
-    List providers.
-
-    Calls the 'listProviders' RPC endpoint of the Unity Catalog service.
-    Returns array of ProviderInfos.
-
-    """
-    proviers_json = UnityCatalogApi(api_client).list_providers()
-    click.echo(mc_pretty_format(proviers_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Get a provider.')
-@click.option('--name', required=True,
-              help='Name of the provider to get.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def get_provider_cli(api_client, name):
-    """
-    Get a provider.
-
-    Calls the 'getProvider' RPC endpoint of the Unity Catalog service.
-    Returns ProviderInfo.
-
-    """
-    provier_json = UnityCatalogApi(api_client).get_provider(name)
-    click.echo(mc_pretty_format(provier_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Update a provider.')
-@click.option('--name', required=True, help='Name of the provider to update.')
-@click.option('--new_name', default=None, help='New name of the provider.')
-@click.option('--comment', default=None, required=False,
-              help='Free-form text description.')
-@click.option('--json-file', default=None, type=click.Path(),
-              help='File containing JSON provider profile.')
-@click.option('--json', default=None, type=JsonClickType(),
-              help=JsonClickType.help('/api/2.0/unity-catalog/providers'))
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def update_provider_cli(api_client, name, new_name, comment, json_file, json):
-    """
-    Update a provider.
-
-    Calls the 'updateProvider' RPC endpoint of the Unity Catalog service.
-    Returns the updated provider info.
-
-    """
-    if json is None and json_file is None:
-        updated_provider = UnityCatalogApi(api_client).update_provider(name, new_name, comment)
-        click.echo(mc_pretty_format(updated_provider))
-    else:
-        json_cli_base(json_file, json,
-                      lambda json: UnityCatalogApi(api_client).update_provider(name, new_name,
-                                                                           comment, json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='List shares of a provider.')
-@click.option('--name', required=True,
-              help='Name of the provider.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def list_provider_shares_cli(api_client, name):
-    """
-    List a provider's shares.
-
-    Calls the 'listProviderShares' RPC endpoint of the Unity Catalog service.
-    Returns array of ProviderShare.
-
-    """
-    shares_json = UnityCatalogApi(api_client).list_provider_shares(name)
-    click.echo(mc_pretty_format(shares_json))
-
-
-@click.command(context_settings=CONTEXT_SETTINGS,
-               short_help='Delete a provider.')
-@click.option('--name', required=True,
-              help='Name of the provider to delete.')
-@debug_option
-@profile_option
-@eat_exceptions
-@provide_api_client
-def delete_provider_cli(api_client, name):
-    """
-    Delete a provider.
-
-    Calls the 'deleteProvider' RPC endpoint of the Unity Catalog service.
-    Returns nothing.
-
-    """
-    UnityCatalogApi(api_client).delete_provider(name)
+from databricks_cli.unity_catalog.metastore_cli import register_metastore_commands
+from databricks_cli.unity_catalog.catalog_cli import register_catalog_commands
+from databricks_cli.unity_catalog.schema_cli import register_schema_commands
+from databricks_cli.unity_catalog.table_cli import register_table_commands
+from databricks_cli.unity_catalog.ext_loc_cli import register_ext_loc_commands
+from databricks_cli.unity_catalog.cred_cli import register_cred_commands
+from databricks_cli.unity_catalog.delta_sharing_cli import register_delta_sharing_commands
+from databricks_cli.unity_catalog.perms_cli import register_perms_commands
 
 
 @click.group(context_settings=CONTEXT_SETTINGS,
@@ -1434,74 +55,11 @@ def unity_catalog_group():  # pragma: no cover
     pass
 
 
-# Metastore cmds:
-unity_catalog_group.add_command(create_metastore_cli, name='create-metastore')
-unity_catalog_group.add_command(list_metastores_cli, name='list-metastores')
-unity_catalog_group.add_command(get_metastore_cli, name='get-metastore')
-unity_catalog_group.add_command(update_metastore_cli, name='update-metastore')
-unity_catalog_group.add_command(delete_metastore_cli, name='delete-metastore')
-unity_catalog_group.add_command(metastore_summary_cli, name='metastore-summary')
-unity_catalog_group.add_command(assign_metastore_cli, name='assign-metastore')
-# Catalogs cmds:
-unity_catalog_group.add_command(create_catalog_cli, name='create-catalog')
-unity_catalog_group.add_command(list_catalogs_cli, name='list-catalogs')
-unity_catalog_group.add_command(get_catalog_cli, name='get-catalog')
-unity_catalog_group.add_command(update_catalog_cli, name='update-catalog')
-unity_catalog_group.add_command(delete_catalog_cli, name='delete-catalog')
-# Schema cmds:
-unity_catalog_group.add_command(create_schema_cli, name='create-schema')
-unity_catalog_group.add_command(list_schemas_cli, name='list-schemas')
-unity_catalog_group.add_command(get_schema_cli, name='get-schema')
-unity_catalog_group.add_command(update_schema_cli, name='update-schema')
-unity_catalog_group.add_command(delete_schema_cli, name='delete-schema')
-# Table cmds:
-unity_catalog_group.add_command(create_table_cli, name='create-table')
-unity_catalog_group.add_command(list_tables_cli, name='list-tables')
-unity_catalog_group.add_command(list_tables_bulk_cli, name='list-tables-bulk')
-unity_catalog_group.add_command(get_table_cli, name='get-table')
-unity_catalog_group.add_command(update_table_cli, name='update-table')
-unity_catalog_group.add_command(delete_table_cli, name='delete-table')
-# DAC cmds:
-unity_catalog_group.add_command(create_dac_cli, name='create-dac')
-unity_catalog_group.add_command(list_dacs_cli, name='list-dacs')
-unity_catalog_group.add_command(get_dac_cli, name='get-dac')
-unity_catalog_group.add_command(delete_dac_cli, name='delete-dac')
-# Credential cmds:
-unity_catalog_group.add_command(create_credential_cli, name='create-storage-credential')
-unity_catalog_group.add_command(list_credentials_cli, name='list-storage-credentials')
-unity_catalog_group.add_command(get_credential_cli, name='get-storage-credential')
-unity_catalog_group.add_command(update_credential_cli, name='update-storage-credential')
-unity_catalog_group.add_command(delete_credential_cli, name='delete-storage-credential')
-# External Location cmds:
-unity_catalog_group.add_command(create_location_cli, name='create-external-location')
-unity_catalog_group.add_command(list_locations_cli, name='list-external-locations')
-unity_catalog_group.add_command(get_location_cli, name='get-external-location')
-unity_catalog_group.add_command(update_location_cli, name='update-external-location')
-unity_catalog_group.add_command(delete_location_cli, name='delete-external-location')
-# Permissions cmds:
-unity_catalog_group.add_command(get_permissions_cli, name='get-permissions')
-unity_catalog_group.add_command(update_permissions_cli, name='update-permissions')
-# replacePermissions endpoint not implemented on MC yet
-# unity_catalog_group.add_command(replace_permissions_cli, name='replace-permissions')
-unity_catalog_group.add_command(create_root_credentials_cli, name='create-root-credentials')
-# Share cmds:
-unity_catalog_group.add_command(create_share_cli, name='create-share')
-unity_catalog_group.add_command(list_shares_cli, name='list-shares')
-unity_catalog_group.add_command(get_share_cli, name='get-share')
-unity_catalog_group.add_command(update_share_cli, name='update-share')
-unity_catalog_group.add_command(delete_share_cli, name='delete-share')
-# Recipient cmds:
-unity_catalog_group.add_command(create_recipient_cli, name='create-recipient')
-unity_catalog_group.add_command(list_recipients_cli, name='list-recipients')
-unity_catalog_group.add_command(get_recipient_cli, name='get-recipient')
-unity_catalog_group.add_command(rotate_recipient_token_cli, name='rotate-recipient-token')
-unity_catalog_group.add_command(get_recipient_share_permissions_cli,
-                                name='get-recipient-share-perms')
-unity_catalog_group.add_command(delete_recipient_cli, name='delete-recipient')
-# Provider cmds:
-unity_catalog_group.add_command(create_provider_cli, name='create-provider')
-unity_catalog_group.add_command(list_providers_cli, name='list-providers')
-unity_catalog_group.add_command(get_provider_cli, name='get-provider')
-unity_catalog_group.add_command(update_provider_cli, name='update-provider')
-unity_catalog_group.add_command(delete_provider_cli, name='delete-provider')
-unity_catalog_group.add_command(list_provider_shares_cli, name='list-provider-shares')
+register_metastore_commands(unity_catalog_group)
+register_ext_loc_commands(unity_catalog_group)
+register_cred_commands(unity_catalog_group)
+register_catalog_commands(unity_catalog_group)
+register_schema_commands(unity_catalog_group)
+register_table_commands(unity_catalog_group)
+register_delta_sharing_commands(unity_catalog_group)
+register_perms_commands(unity_catalog_group)
diff --git a/databricks_cli/unity_catalog/cred_cli.py b/databricks_cli/unity_catalog/cred_cli.py
new file mode 100644
index 00000000..e9ed72b6
--- /dev/null
+++ b/databricks_cli/unity_catalog/cred_cli.py
@@ -0,0 +1,247 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import DacIdClickType, JsonClickType, MetastoreIdClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+#############  Storage Credential Commands  ############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create storage credential.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/storage-credentials'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_credential_cli(api_client, json_file, json):
+    """
+    Create new storage credential.
+
+    Calls the 'createStorageCredential' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the properties of the newly-created Storage Credential.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_storage_credential(json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List storage credentials.')
+@click.option('--name-pattern', default=None,
+              help='SQL LIKE pattern that the credential name must match to be in list.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_credentials_cli(api_client, name_pattern):
+    """
+    List storage credentials.
+
+    Calls the 'listStorageCredentials' RPC endpoint of the Unity Catalog service.
+    Returns array of StorageCredentials.
+
+    """
+    creds_json = UnityCatalogApi(api_client).list_storage_credentials(name_pattern)
+    click.echo(mc_pretty_format(creds_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a storage credential.')
+@click.option('--name', required=True,
+              help='Name of the storage credential to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_credential_cli(api_client, name):
+    """
+    Get a storage credential.
+
+    Calls the 'getStorageCredential' RPC endpoint of the Unity Catalog service.
+    Returns an StorageCredential object.
+
+    """
+    cred_json = UnityCatalogApi(api_client).get_storage_credential(name)
+    click.echo(mc_pretty_format(cred_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a storage credential.')
+@click.option('--name', required=True,
+              help='Name of the storage credential to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/storage-credentials'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_credential_cli(api_client, name, json_file, json):
+    """
+    Update a storage credential.
+
+    Calls the 'updateStorageCredential' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_storage_credential(name, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a storage credential.')
+@click.option('--name', required=True,
+              help='Name of the storage credential to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_credential_cli(api_client, name):
+    """
+    Delete a storage credential.
+
+    Calls the 'deleteStorageCredential' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_storage_credential(name)
+
+
+#############  Data Access Configuration Commands  ############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create data access configuration.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/data-access-configurations'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_dac_cli(api_client, metastore_id, json_file, json):
+    """
+    Create new data access configuration.
+
+    Calls the 'createDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the properties of the newly-created DAC.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_dac(metastore_id, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List data access configurations.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC(s).')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_dacs_cli(api_client, metastore_id):
+    """
+    List data access configurations.
+
+    Calls the 'listDataAccessConfigurations' RPC endpoint of the Unity Catalog service.
+    Returns array of DataAccessConfigurations.
+
+    """
+    dacs_json = UnityCatalogApi(api_client).list_dacs(metastore_id)
+    click.echo(mc_pretty_format(dacs_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get data access configuration.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC.')
+@click.option('--dac-id', required=True, type=DacIdClickType(),
+              help='Data access configuration ID.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_dac_cli(api_client, metastore_id, dac_id):
+    """
+    Get data access configuration details.
+
+    Calls the 'getDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
+    Returns details of the DAC specified by its id (TODO: lookup by DAC name?).
+
+    """
+    dac_json = UnityCatalogApi(api_client).get_dac(metastore_id, dac_id)
+    click.echo(mc_pretty_format(dac_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete data access configuration.')
+@click.option('--metastore-id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore parent of the DAC.')
+@click.option('--dac-id', required=True, type=DacIdClickType(),
+              help='Data access configuration ID.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_dac_cli(api_client, metastore_id, dac_id):
+    """
+    Delete data access configuration details.
+
+    Calls the 'deleteDataAccessConfiguration' RPC endpoint of the Unity Catalog service.
+
+    """
+    UnityCatalogApi(api_client).delete_dac(metastore_id, dac_id)
+
+
+def register_cred_commands(cmd_group):
+    # Storage Credential cmds:
+    cmd_group.add_command(create_credential_cli, name='create-storage-credential')
+    cmd_group.add_command(list_credentials_cli, name='list-storage-credentials')
+    cmd_group.add_command(get_credential_cli, name='get-storage-credential')
+    cmd_group.add_command(update_credential_cli, name='update-storage-credential')
+    cmd_group.add_command(delete_credential_cli, name='delete-storage-credential')
+
+    # DAC cmds: [TO BE DEPRECATED ONCE STORAGE CREDENTIALS ARE FULLY SUPPORTED]
+    cmd_group.add_command(create_dac_cli, name='create-dac')
+    cmd_group.add_command(list_dacs_cli, name='list-dacs')
+    cmd_group.add_command(get_dac_cli, name='get-dac')
+    cmd_group.add_command(delete_dac_cli, name='delete-dac')
diff --git a/databricks_cli/unity_catalog/delta_sharing_cli.py b/databricks_cli/unity_catalog/delta_sharing_cli.py
new file mode 100644
index 00000000..b218653a
--- /dev/null
+++ b/databricks_cli/unity_catalog/delta_sharing_cli.py
@@ -0,0 +1,484 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import JsonClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+##############  Share Commands  ##############
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new share.')
+@click.option('--name', required=True, help='Name of new share.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_share_cli(api_client, name):
+    """
+    Create a new share.
+
+    Calls the 'createShare' RPC endpoint of the Unity Catalog service.
+    Returns the ShareInfo for the newly-created share.
+
+    """
+    share_json = UnityCatalogApi(api_client).create_share(name)
+    click.echo(mc_pretty_format(share_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List shares.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_shares_cli(api_client):
+    """
+    List shares.
+
+    Calls the 'listShares' RPC endpoint of the Unity Catalog service.
+    Returns array of ShareInfos.
+
+    """
+    shares_json = UnityCatalogApi(api_client).list_shares()
+    click.echo(mc_pretty_format(shares_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a share.')
+@click.option('--name', required=True,
+              help='Name of the share to get.')
+@click.option('--include-shared-data', default=True,
+              help='Whether to include shared data in the response.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_share_cli(api_client, name, include_shared_data):
+    """
+    Get a share.
+
+    Calls the 'getShare' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    share_json = UnityCatalogApi(api_client).get_share(name, include_shared_data)
+    click.echo(mc_pretty_format(share_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List permissions on a share.')
+@click.option('--name', required=True,
+              help='Name of the share to list permissions on.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_share_permissions_cli(api_client, name):
+    """
+    List permissions on a share.
+
+    Calls the 'getSharePermissions' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    perms_json = UnityCatalogApi(api_client).list_share_permissions(name)
+    click.echo(mc_pretty_format(perms_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update permissions on a share.')
+@click.option('--name', required=True,
+              help='Name of the share whose permissions are updated.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/shares/{name}/permissions'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_share_permissions_cli(api_client, name, json_file, json):
+    """
+    List permissions on a share.
+
+    Calls the 'getSharePermissions' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_share_permissions(name, json))
+
+
+def shared_data_object(name):
+    return {'name': name, 'data_object_type': 'TABLE'}
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a share.')
+@click.option('--name', required=True,
+              help='Name of the share to update.')
+@click.option('--add-table', default=None, multiple=True,
+              help='Full name of table to add to share')
+@click.option('--remove-table', default=None, multiple=True,
+              help='Full name of table to remove from share')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/shares'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_share_cli(api_client, name, add_table, remove_table, json_file, json):
+    """
+    Update a share.
+
+    Calls the 'updateShare' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    if len(add_table) > 0 or len(remove_table) > 0:
+        updates = []
+        for a in add_table:
+            updates.append({'action': 'ADD', 'data_object': shared_data_object(a)})
+        for r in remove_table:
+            updates.append({'action': 'REMOVE', 'data_object': shared_data_object(r)})
+        d = {'updates': updates}
+        share_json = UnityCatalogApi(api_client).update_share(name, d)
+        click.echo(mc_pretty_format(share_json))
+    else:
+        json_cli_base(json_file, json,
+                      lambda json: UnityCatalogApi(api_client).update_share(name, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a share.')
+@click.option('--name', required=True,
+              help='Name of the share to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_share_cli(api_client, name):
+    """
+    Delete a share.
+
+    Calls the 'deleteShare' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_share(name)
+
+
+##############  Recipient Commands  ##############
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new recipient.')
+@click.option('--name', required=True, help='Name of new recipient.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_recipient_cli(api_client, name, comment):
+    """
+    Create a new recipient.
+
+    Calls the 'createRecipient' RPC endpoint of the Unity Catalog service.
+    Returns the RecipientInfo for the newly-created recipient.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).create_recipient(name, comment)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List recipients.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_recipients_cli(api_client):
+    """
+    List recipients.
+
+    Calls the 'listRecipients' RPC endpoint of the Unity Catalog service.
+    Returns array of RecipientInfos.
+
+    """
+    recipients_json = UnityCatalogApi(api_client).list_recipients()
+    click.echo(mc_pretty_format(recipients_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a recipient.')
+@click.option('--name', required=True,
+              help='Name of the recipient to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_recipient_cli(api_client, name):
+    """
+    Get a recipient.
+
+    Calls the 'getRecipient' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).get_recipient(name)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Rotate token for the recipient.')
+@click.option('--name', required=True, help='Name of new recipient.')
+@click.option('--existing-token-expire-in-seconds', default=None, required=False,
+              help='Expire the existing token in number of seconds from now,' +
+                   ' 0 to expire it immediately.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def rotate_recipient_token_cli(api_client, name, existing_token_expire_in_seconds):
+    """
+    Rotate recipient token.
+
+    Calls the 'rotateRecipientToken' RPC endpoint of the Unity Catalog service.
+    Returns the RecipientInfo for the recipient with rotated tokens.
+
+    """
+    recipient_json = \
+        UnityCatalogApi(api_client).rotate_recipient_token(name, existing_token_expire_in_seconds)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List share permissions of a recipient.')
+@click.option('--name', required=True,
+              help='Name of the recipient.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_recipient_permissions_cli(api_client, name):
+    """
+    List a recipient's share permissions.
+
+    Calls the 'getRecipientSharePermissions' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    recipient_json = UnityCatalogApi(api_client).get_recipient_share_permissions(name)
+    click.echo(mc_pretty_format(recipient_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a recipient.')
+@click.option('--name', required=True,
+              help='Name of the recipient to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_recipient_cli(api_client, name):
+    """
+    Delete a recipient.
+
+    Calls the 'deleteRecipient' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_recipient(name)
+
+
+##############  Provider Commands  ##############
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a provider.')
+@click.option('--name', required=True, help='Name of the new provider.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@click.option('--recipient-profile-json-file', default=None, type=click.Path(),
+              help='File containing recipient profile in JSON format.')
+@click.option('--recipient-profile-json', default=None, type=JsonClickType(),
+              help='JSON string containing recipient profile.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_provider_cli(api_client, name, comment, recipient_profile_json_file,
+                        recipient_profile_json):
+    """
+    Create a provider.
+
+    Calls the 'createProvider' RPC endpoint of the Unity Catalog service.
+    Returns the created provider info.
+
+    """
+    json_cli_base(recipient_profile_json_file, recipient_profile_json,
+                  lambda json: UnityCatalogApi(api_client).create_provider(name, comment, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List providers.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_providers_cli(api_client):
+    """
+    List providers.
+
+    Calls the 'listProviders' RPC endpoint of the Unity Catalog service.
+    Returns array of ProviderInfos.
+
+    """
+    proviers_json = UnityCatalogApi(api_client).list_providers()
+    click.echo(mc_pretty_format(proviers_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a provider.')
+@click.option('--name', required=True,
+              help='Name of the provider to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_provider_cli(api_client, name):
+    """
+    Get a provider.
+
+    Calls the 'getProvider' RPC endpoint of the Unity Catalog service.
+    Returns ProviderInfo.
+
+    """
+    provier_json = UnityCatalogApi(api_client).get_provider(name)
+    click.echo(mc_pretty_format(provier_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a provider.')
+@click.option('--name', required=True, help='Name of the provider to update.')
+@click.option('--new_name', default=None, help='New name of the provider.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@click.option('--recipient-profile-json-file', default=None, type=click.Path(),
+              help='File containing recipient profile in JSON format.')
+@click.option('--recipient-profile-json', default=None, type=JsonClickType(),
+              help='JSON string containing recipient profile.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_provider_cli(api_client, name, new_name, comment, recipient_profile_json_file,
+                        recipient_profile_json):
+    """
+    Update a provider.
+
+    Calls the 'updateProvider' RPC endpoint of the Unity Catalog service.
+    Returns the updated provider info.
+
+    """
+    if recipient_profile_json is None and recipient_profile_json_file is None:
+        updated_provider = UnityCatalogApi(api_client).update_provider(name, new_name, comment)
+        click.echo(mc_pretty_format(updated_provider))
+    else:
+        json_cli_base(recipient_profile_json_file, recipient_profile_json,
+                      lambda json: UnityCatalogApi(api_client).update_provider(name, new_name,
+                                                                               comment, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List shares of a provider.')
+@click.option('--name', required=True,
+              help='Name of the provider.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_provider_shares_cli(api_client, name):
+    """
+    List a provider's shares.
+
+    Calls the 'listProviderShares' RPC endpoint of the Unity Catalog service.
+    Returns array of ProviderShare.
+
+    """
+    shares_json = UnityCatalogApi(api_client).list_provider_shares(name)
+    click.echo(mc_pretty_format(shares_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a provider.')
+@click.option('--name', required=True,
+              help='Name of the provider to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_provider_cli(api_client, name):
+    """
+    Delete a provider.
+
+    Calls the 'deleteProvider' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_provider(name)
+
+
+def register_delta_sharing_commands(cmd_group):
+    # Share cmds:
+    cmd_group.add_command(create_share_cli, name='create-share')
+    cmd_group.add_command(list_shares_cli, name='list-shares')
+    cmd_group.add_command(get_share_cli, name='get-share')
+    cmd_group.add_command(update_share_cli, name='update-share')
+    cmd_group.add_command(delete_share_cli, name='delete-share')
+    cmd_group.add_command(list_share_permissions_cli, name='list-share-permissions')
+    cmd_group.add_command(update_share_permissions_cli, name='update-share-permissions')
+
+    # Recipient cmds:
+    cmd_group.add_command(create_recipient_cli, name='create-recipient')
+    cmd_group.add_command(list_recipients_cli, name='list-recipients')
+    cmd_group.add_command(get_recipient_cli, name='get-recipient')
+    cmd_group.add_command(rotate_recipient_token_cli, name='rotate-recipient-token')
+    cmd_group.add_command(list_recipient_permissions_cli, name='list-recipient-permissions')
+    cmd_group.add_command(delete_recipient_cli, name='delete-recipient')
+
+    # Provider cmds:
+    cmd_group.add_command(create_provider_cli, name='create-provider')
+    cmd_group.add_command(list_providers_cli, name='list-providers')
+    cmd_group.add_command(get_provider_cli, name='get-provider')
+    cmd_group.add_command(update_provider_cli, name='update-provider')
+    cmd_group.add_command(delete_provider_cli, name='delete-provider')
+    cmd_group.add_command(list_provider_shares_cli, name='list-provider-shares')
diff --git a/databricks_cli/unity_catalog/ext_loc_cli.py b/databricks_cli/unity_catalog/ext_loc_cli.py
new file mode 100644
index 00000000..f1101f95
--- /dev/null
+++ b/databricks_cli/unity_catalog/ext_loc_cli.py
@@ -0,0 +1,165 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import JsonClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create External Location.')
+@click.option('--name', default=None,
+              help='Name of new external location')
+@click.option('--url', default=None,
+              help='Path URL for the new external location')
+@click.option('--storage-credential-name', default=None,
+              help='Name of storage credential to use with new external location')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/external-locations'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_location_cli(api_client, name, url, storage_credential_name, json_file, json):
+    """
+    Create new external location.
+
+    Calls the 'createExternalLocation' RPC endpoint of the Unity Catalog service.
+    The specification for the request json can be found at <insert doc link here>.
+    Returns the properties of the newly-created Storage Credential.
+
+    """
+    if (name is not None) and (url is not None) and (storage_credential_name is not None):
+        if (json_file is not None) or (json is not None):
+            raise ValueError('Cannot specify JSON if both name and url are given')
+        data = {"name": name, "url": url, "credential_name": storage_credential_name}
+        loc_json = UnityCatalogApi(api_client).create_external_location(data)
+        click.echo(mc_pretty_format(loc_json))
+    elif (json is None) and (json_file is None):
+        raise ValueError('Must provide name, url and storage-credential-name' +
+                         ' or use JSON specification')
+    else:
+        json_cli_base(json_file, json,
+                      lambda json:
+                      UnityCatalogApi(api_client).create_external_location(json), encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List external locations.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_locations_cli(api_client, ):
+    """
+    List external locations.
+
+    Calls the 'listExternalLocations' RPC endpoint of the Unity Catalog service.
+    Returns array of ExternalLocations.
+
+    """
+    locs_json = UnityCatalogApi(api_client).list_external_locations()
+    click.echo(mc_pretty_format(locs_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get an external location.')
+@click.option('--name', required=True,
+              help='Name of the external location to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_location_cli(api_client, name):
+    """
+    Get an external location.
+
+    Calls the 'getExternalLocation' RPC endpoint of the Unity Catalog service.
+    Returns an ExternalLocation object.
+
+    """
+    loc_json = UnityCatalogApi(api_client).get_external_location(name)
+    click.echo(mc_pretty_format(loc_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update an external location.')
+@click.option('--name', required=True,
+              help='Name of the external location to update.')
+@click.option('--force', '-f', is_flag=True, default=False,
+              help='Force update even if location has dependent tables/mounts')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/external-locations'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_location_cli(api_client, name, force, json_file, json):
+    """
+    Update an external location.
+
+    Calls the 'updateExternalLocation' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_external_location(name, force,
+                                                                                    json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete an external location.')
+@click.option('--name', required=True,
+              help='Name of the external location to delete.')
+@click.option('--force', '-f', is_flag=True, default=False,
+              help='Force deletion even if location has dependent tables/mounts')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_location_cli(api_client, name, force):
+    """
+    Delete an external location.
+
+    Calls the 'deleteExternalLocation' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_external_location(name, force)
+
+
+def register_ext_loc_commands(cmd_group):
+    cmd_group.add_command(create_location_cli, name='create-external-location')
+    cmd_group.add_command(list_locations_cli, name='list-external-locations')
+    cmd_group.add_command(get_location_cli, name='get-external-location')
+    cmd_group.add_command(update_location_cli, name='update-external-location')
+    cmd_group.add_command(delete_location_cli, name='delete-external-location')
diff --git a/databricks_cli/unity_catalog/metastore_cli.py b/databricks_cli/unity_catalog/metastore_cli.py
new file mode 100644
index 00000000..b47cb739
--- /dev/null
+++ b/databricks_cli/unity_catalog/metastore_cli.py
@@ -0,0 +1,218 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import MetastoreIdClickType, WorkspaceIdClickType, JsonClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+#################  Metastore Commands  #####################
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a metastore.')
+@click.option('--name', required=True, help='Name of the new metastore.')
+@click.option('--storage-root', required=True,
+              help='Storage root URL for the new metastore.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_metastore_cli(api_client, name, storage_root):
+    """
+    Create new metastore specified by the JSON input.
+
+    Calls the 'createMetastore' RPC endpoint of the Unity Catalog service.
+    Returns the properties of the newly-created metastore.
+
+    """
+    metastore_json = UnityCatalogApi(api_client).create_metastore(name, storage_root)
+    click.echo(mc_pretty_format(metastore_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List metastores.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_metastores_cli(api_client):
+    """
+    List metastores.
+
+    Calls the 'listMetastores' RPC endpoint of the Unity Catalog service.
+    Returns array of MetastoreInfos.
+
+    """
+    metastores_json = UnityCatalogApi(api_client).list_metastores()
+    click.echo(mc_pretty_format(metastores_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a metastore.')
+@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_metastore_cli(api_client, metastore_id):
+    """
+    Get a metastore.
+
+    Calls the 'getMetastore' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    metastore_json = UnityCatalogApi(api_client).get_metastore(metastore_id)
+    click.echo(mc_pretty_format(metastore_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a metastore.')
+@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/admin/metastores'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_metastore_cli(api_client, metastore_id, json_file, json):
+    """
+    Update a metastore.
+
+    Calls the 'updateMetastore' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_metastore(metastore_id, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a metastore.')
+@click.option('--id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to delete.')
+@click.option('--force', '-f', is_flag=True, default=False)
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_metastore_cli(api_client, metastore_id, force):
+    """
+    Delete a metastore.
+
+    Calls the 'deleteMetastore' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_metastore(metastore_id, force)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get summary info of current metastore.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def metastore_summary_cli(api_client):
+    """
+    Get metastore summary.
+
+    Calls the 'getMetastoreSummary' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    summary_json = UnityCatalogApi(api_client).get_metastore_summary()
+    click.echo(mc_pretty_format(summary_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Assign a metastore to a workspace.')
+@click.option('--workspace-id', 'workspace_id', required=True, type=WorkspaceIdClickType(),
+              help='Unique identifier of the workspace for the metastore assignment.')
+@click.option('--metastore-id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to assign to the workspace.')
+@click.option('--default-catalog-name', 'default_catalog_name', required=False,
+              default='hive_metastore',
+              help='Name of the default catalog to use with the metastore ' +
+                   '(default: "hive_metastore").')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def assign_metastore_cli(api_client, workspace_id, metastore_id, default_catalog_name):
+    """
+    Assign a metastore to a specified workspace.
+
+    Calls the 'createMetastoreAssignment' RPC endpoint of the Unity Catalog service.
+    If that fails due to the workspace already having a Metastore assigned, it calls
+    the 'updateMetastoreAssignment' endpoint.
+    Returns nothing.
+
+    """
+    resp = UnityCatalogApi(api_client).create_metastore_assignment(workspace_id, metastore_id,
+                                                                   default_catalog_name)
+    # resp will just be an empty object ('{}') but it's good to print *something*
+    click.echo(mc_pretty_format(resp))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Unassigns a metastore from a workspace.')
+@click.option('--workspace-id', 'workspace_id', required=True, type=WorkspaceIdClickType(),
+              help='Unique identifier of the workspace.')
+@click.option('--metastore-id', 'metastore_id', required=True, type=MetastoreIdClickType(),
+              help='Unique identifier of the metastore to unassign from the workspace.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def unassign_metastore_cli(api_client, workspace_id, metastore_id):
+    """
+    Unassign a metastore from a workspace.
+
+    Calls the 'deleteMetastoreAssignment' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    resp = UnityCatalogApi(api_client).delete_metastore_assignment(workspace_id, metastore_id)
+    # resp will just be an empty object ('{}') but it's good to print *something*
+    click.echo(mc_pretty_format(resp))
+
+
+def register_metastore_commands(cmd_group):
+    cmd_group.add_command(create_metastore_cli, name='create-metastore')
+    cmd_group.add_command(list_metastores_cli, name='list-metastores')
+    cmd_group.add_command(get_metastore_cli, name='get-metastore')
+    cmd_group.add_command(update_metastore_cli, name='update-metastore')
+    cmd_group.add_command(delete_metastore_cli, name='delete-metastore')
+    cmd_group.add_command(metastore_summary_cli, name='metastore-summary')
+    cmd_group.add_command(assign_metastore_cli, name='assign-metastore')
+    cmd_group.add_command(unassign_metastore_cli, name='unassign-metastore')
diff --git a/databricks_cli/unity_catalog/perms_cli.py b/databricks_cli/unity_catalog/perms_cli.py
new file mode 100644
index 00000000..39334d31
--- /dev/null
+++ b/databricks_cli/unity_catalog/perms_cli.py
@@ -0,0 +1,134 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import JsonClickType, OneOfOption
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+PERMISSIONS_OBJ_TYPES = [
+    'catalog', 'schema', 'table', 'storage-credential', 'external-location'
+]
+
+
+def _get_perm_securable_name_and_type(catalog_name, schema_full_name, table_full_name,
+                                      credential_name, location_name):
+    if catalog_name:
+        return ('catalog', catalog_name)
+    elif schema_full_name:
+        return ('schema', schema_full_name)
+    elif table_full_name:
+        return ('table', table_full_name)
+    elif credential_name:
+        return ('storage-credential', credential_name)
+    else:
+        return ('external-location', location_name)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get permissions on a securable.')
+@click.option('--catalog', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of catalog of interest')
+@click.option('--schema', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of schema of interest')
+@click.option('--table', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of table of interest')
+@click.option('--storage-credential', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the storage credential of interest')
+@click.option('--external-location', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the external location of interest')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_permissions_cli(api_client, catalog, schema, table, storage_credential,
+                        external_location):
+    """
+    Get permissions on a securable.
+
+    Calls the 'getPermissions' RPC endpoint of the Unity Catalog service.
+    Returns PermissionsList for the requested securable.
+
+    """
+    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table,
+                                                           storage_credential, external_location)
+
+    perm_json = UnityCatalogApi(api_client).get_permissions(sec_type, sec_name)
+    click.echo(mc_pretty_format(perm_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='update permissions on a securable.')
+@click.option('--catalog', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of catalog of interest')
+@click.option('--schema', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of schema of interest')
+@click.option('--table', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Full name of table of interest')
+@click.option('--storage-credential', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the storage credential of interest')
+@click.option('--external-location', cls=OneOfOption, default=None,
+              one_of=PERMISSIONS_OBJ_TYPES,
+              help='Name of the external location of interest')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON of permissions change to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/permissions'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_permissions_cli(api_client, catalog, schema, table, storage_credential,
+                           external_location, json_file, json):
+    """
+    Update permissions on a securable.
+
+    Calls the 'updatePermissions' RPC endpoint of the Unity Catalog service.
+    Returns updated PermissionsList for the requested securable.
+
+    """
+    sec_type, sec_name = _get_perm_securable_name_and_type(catalog, schema, table,
+                                                           storage_credential, external_location)
+
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_permissions(sec_type, sec_name,
+                                                                              json),
+                  encode_utf8=True)
+
+
+def register_perms_commands(cmd_group):
+    cmd_group.add_command(get_permissions_cli, name='get-permissions')
+    cmd_group.add_command(update_permissions_cli, name='update-permissions')
diff --git a/databricks_cli/unity_catalog/schema_cli.py b/databricks_cli/unity_catalog/schema_cli.py
new file mode 100644
index 00000000..082f177f
--- /dev/null
+++ b/databricks_cli/unity_catalog/schema_cli.py
@@ -0,0 +1,146 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import JsonClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a new schema.')
+@click.option('--catalog-name', required=True, help='Parent catalog of new schema.')
+@click.option('--name', required=True,
+              help='Name of new schema, relative to parent catalog.')
+@click.option('--comment', default=None, required=False,
+              help='Free-form text description.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_schema_cli(api_client, catalog_name, name, comment):
+    """
+    Create a new schema in the specified catalog.
+
+    Calls the 'createSchema' RPC endpoint of the Unity Catalog service.
+    Returns the SchemaInfo for the newly-created schema.
+
+    """
+    schema_json = UnityCatalogApi(api_client).create_schema(catalog_name, name, comment)
+    click.echo(mc_pretty_format(schema_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List schemas.')
+@click.option('--catalog-name', required=True,
+              help='Name of the parent catalog for schemas of interest.')
+@click.option('--name-pattern', default=None,
+              help='SQL LIKE pattern that the schema name must match to be in list.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_schemas_cli(api_client, catalog_name, name_pattern):
+    """
+    List schemas.
+
+    Calls the 'listSchemas' RPC endpoint of the Unity Catalog service.
+    Returns array of SchemaInfos.
+
+    """
+    schemas_json = UnityCatalogApi(api_client).list_schemas(catalog_name, name_pattern)
+    click.echo(mc_pretty_format(schemas_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a schema.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>) of the schema to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_schema_cli(api_client, full_name):
+    """
+    Get a schema.
+
+    Calls the 'getSchema' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    schema_json = UnityCatalogApi(api_client).get_schema(full_name)
+    click.echo(mc_pretty_format(schema_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a schema.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>) of the schema to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/schemas'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_schema_cli(api_client, full_name, json_file, json):
+    """
+    Update a schema.
+
+    Calls the 'updateSchema' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_schema(full_name, json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a schema.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>) of the schema to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_schema_cli(api_client, full_name):
+    """
+    Delete a schema.
+
+    Calls the 'deleteSchema' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_schema(full_name)
+
+
+def register_schema_commands(cmd_group):
+    cmd_group.add_command(create_schema_cli, name='create-schema')
+    cmd_group.add_command(list_schemas_cli, name='list-schemas')
+    cmd_group.add_command(get_schema_cli, name='get-schema')
+    cmd_group.add_command(update_schema_cli, name='update-schema')
+    cmd_group.add_command(delete_schema_cli, name='delete-schema')
diff --git a/databricks_cli/unity_catalog/table_cli.py b/databricks_cli/unity_catalog/table_cli.py
new file mode 100644
index 00000000..235b5ac9
--- /dev/null
+++ b/databricks_cli/unity_catalog/table_cli.py
@@ -0,0 +1,170 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+
+from databricks_cli.click_types import JsonClickType
+from databricks_cli.configure.config import provide_api_client, profile_option, debug_option
+from databricks_cli.unity_catalog.api import UnityCatalogApi
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+from databricks_cli.utils import eat_exceptions, CONTEXT_SETTINGS, json_cli_base
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Create a table.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to POST.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/unity-catalog/tables'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def create_table_cli(api_client, json_file, json):
+    """
+    Create new table specified by the JSON input.
+
+    Calls the 'createTable' RPC endpoint of the Unity Catalog service.
+    Returns the properties of the newly-created table.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).create_table(json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List tables.')
+@click.option('--catalog-name', required=True,
+              help='Name of the parent catalog for tables of interest.')
+@click.option('--schema-name', required=True,
+              help='Name of the parent schema for tables of interest.')
+@click.option('--name-pattern', default=None,
+              help='SQL LIKE pattern that the table name must match to be in list.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_tables_cli(api_client, catalog_name, schema_name, name_pattern):
+    """
+    List tables.
+
+    Calls the 'listTables' RPC endpoint of the Unity Catalog service.
+    Returns array of TableInfos.
+
+    """
+    tables_json = UnityCatalogApi(api_client).list_tables(catalog_name, schema_name, name_pattern)
+    click.echo(mc_pretty_format(tables_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='List table summaries.')
+@click.option('--catalog-name', required=True,
+              help='Name of the parent catalog for tables of interest.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def list_table_summaries_cli(api_client, catalog_name):
+    """
+    List table summaries (in bulk).
+
+    Calls the 'listTableSummaries' RPC endpoint of the Unity Catalog service.
+    Returns array of TableSummarys.
+
+    """
+    tables_json = UnityCatalogApi(api_client).list_table_summaries(catalog_name)
+    click.echo(mc_pretty_format(tables_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Get a table.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>.<table>) of the table to get.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def get_table_cli(api_client, full_name):
+    """
+    Get a table.
+
+    Calls the 'getTable' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    table_json = UnityCatalogApi(api_client).get_table(full_name)
+    click.echo(mc_pretty_format(table_json))
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Update a table.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>.<table>) of the table to update.')
+@click.option('--json-file', default=None, type=click.Path(),
+              help='File containing JSON request to PATCH.')
+@click.option('--json', default=None, type=JsonClickType(),
+              help=JsonClickType.help('/api/2.0/tables'))
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def update_table_cli(api_client, full_name, json_file, json):
+    """
+    Update a table.
+
+    Calls the 'updateTable' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    json_cli_base(json_file, json,
+                  lambda json: UnityCatalogApi(api_client).update_table(full_name, json),
+                  encode_utf8=True)
+
+
+@click.command(context_settings=CONTEXT_SETTINGS,
+               short_help='Delete a table.')
+@click.option('--full-name', required=True,
+              help='Full name (<catalog>.<schema>.<table>) of the table to delete.')
+@debug_option
+@profile_option
+@eat_exceptions
+@provide_api_client
+def delete_table_cli(api_client, full_name):
+    """
+    Delete a table.
+
+    Calls the 'deleteTable' RPC endpoint of the Unity Catalog service.
+    Returns nothing.
+
+    """
+    UnityCatalogApi(api_client).delete_table(full_name)
+
+
+def register_table_commands(cmd_group):
+    cmd_group.add_command(create_table_cli, name='create-table')
+    cmd_group.add_command(list_tables_cli, name='list-tables')
+    cmd_group.add_command(list_table_summaries_cli, name='list-table-summaries')
+    cmd_group.add_command(get_table_cli, name='get-table')
+    cmd_group.add_command(update_table_cli, name='update-table')
+    cmd_group.add_command(delete_table_cli, name='delete-table')
diff --git a/databricks_cli/unity_catalog/uc_service.py b/databricks_cli/unity_catalog/uc_service.py
new file mode 100644
index 00000000..aca10186
--- /dev/null
+++ b/databricks_cli/unity_catalog/uc_service.py
@@ -0,0 +1,448 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from databricks_cli.unity_catalog.utils import mc_pretty_format
+
+
+class UnityCatalogService(object):
+    def __init__(self, client):
+        self.client = client
+
+    # Metastore Operations
+
+    def create_metastore(self, name, storage_root, headers=None):
+        _data = {
+            'name': name,
+            'storage_root': storage_root,
+        }
+        return self.client.perform_query('POST', '/unity-catalog/metastores', data=_data,
+                                         headers=headers)
+
+    def list_metastores(self, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/metastores', data=_data,
+                                         headers=headers)
+
+    def get_metastore(self, metastore_id, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/metastores/%s' % (metastore_id),
+                                         data=_data, headers=headers)
+
+    def get_metastore_summary(self, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/metastore_summary', data=_data,
+                                         headers=headers)
+
+    def update_metastore(self, metastore_id, metastore_spec, headers=None):
+        return self.client.perform_query('PATCH', '/unity-catalog/metastores/%s' % (metastore_id),
+                                         data=metastore_spec, headers=headers)
+
+    def delete_metastore(self, metastore_id, force=None, headers=None):
+        _data = {}
+        if force is not None:
+            _data['force'] = force
+
+        return self.client.perform_query('DELETE', '/unity-catalog/metastores/%s' % (metastore_id),
+                                         data=_data, headers=headers)
+
+    def create_metastore_assignment(self, workspace_id, metastore_id, default_catalog_name=None,
+                                    headers=None):
+        _data = {
+            'metastore_id': metastore_id
+        }
+        if default_catalog_name is not None:
+            _data['default_catalog_name'] = default_catalog_name
+        url = '/unity-catalog/workspaces/%s/metastore' % (workspace_id)
+        return self.client.perform_query('PUT', url, data=_data, headers=headers)
+
+    def update_metastore_assignment(self, workspace_id, metastore_id, default_catalog_name,
+                                    headers=None):
+        _data = {
+            'metastore_id': metastore_id,
+            'default_catalog_name': default_catalog_name
+        }
+        url = '/unity-catalog/workspaces/%s/metastore' % (workspace_id)
+        return self.client.perform_query('PATCH', url, data=_data, headers=headers)
+
+    def delete_metastore_assignment(self, workspace_id, metastore_id, headers=None):
+        _data = {
+            'metastore_id': metastore_id
+        }
+        url = '/unity-catalog/workspaces/%s/metastore' % (workspace_id)
+        return self.client.perform_query('DELETE', url, data=_data, headers=headers)
+
+    # External Location Operations
+
+    def create_external_location(self, loc_spec, headers=None):
+        _data = loc_spec
+        url = '/unity-catalog/external-locations'
+        return self.client.perform_query('POST', url, data=_data, headers=headers)
+
+    def list_external_locations(self, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/external-locations', data=_data,
+                                         headers=headers)
+
+    def get_external_location(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/external-locations/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def update_external_location(self, name, force, loc_spec, headers=None):
+        _data = loc_spec
+        if force:
+            _data["force"] = True
+
+        return self.client.perform_query('PATCH', '/unity-catalog/external-locations/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def delete_external_location(self, name, force, headers=None):
+        _data = {
+            "force": force
+        }
+
+        return self.client.perform_query('DELETE', '/unity-catalog/external-locations/%s' % (name),
+                                         data=_data, headers=headers)
+
+    # Data Access Configuration Operations
+
+    def create_dac(self, metastore_id, dac_spec, headers=None):
+        _data = dac_spec
+        url = '/unity-catalog/metastores/%s/data-access-configurations' % (metastore_id)
+        return self.client.perform_query('POST', url, data=_data, headers=headers)
+
+    def list_dacs(self, metastore_id, headers=None):
+        _data = {}
+
+        url = '/unity-catalog/metastores/%s/data-access-configurations' % (metastore_id)
+        return self.client.perform_query('GET', url, data=_data, headers=headers)
+
+    def get_dac(self, metastore_id, dac_id, headers=None):
+        url = '/unity-catalog/metastores/%s/data-access-configurations/%s' % (metastore_id, dac_id)
+        return self.client.perform_query('GET', url, headers=headers)
+
+    def delete_dac(self, metastore_id, dac_id, headers=None):
+        url = '/unity-catalog/metastores/%s/data-access-configurations/%s' % (metastore_id, dac_id)
+        return self.client.perform_query('DELETE', url, headers=headers)
+
+    # Storage Credential Operations
+
+    def create_storage_credential(self, cred_spec, headers=None):
+        _data = cred_spec
+        url = '/unity-catalog/storage-credentials'
+        return self.client.perform_query('POST', url, data=_data, headers=headers)
+
+    def list_storage_credentials(self, name_pattern=None, headers=None):
+        _data = {}
+        if name_pattern is not None:
+            _data['name_pattern'] = name_pattern
+
+        return self.client.perform_query('GET', '/unity-catalog/storage-credentials',
+                                         data=_data, headers=headers)
+
+    def get_storage_credential(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/storage-credentials/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def update_storage_credential(self, name, cred_spec, headers=None):
+        return self.client.perform_query('PATCH', '/unity-catalog/storage-credentials/%s' % (name),
+                                         data=cred_spec, headers=headers)
+
+    def delete_storage_credential(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('DELETE', '/unity-catalog/storage-credentials/%s' % (name),
+                                         data=_data, headers=headers)
+
+    # Catalog Operations
+
+    def create_catalog(self, name, comment=None, provider=None, share=None, headers=None):
+        _data = {
+            'name': name,
+        }
+        if comment is not None:
+            _data['comment'] = comment
+        if provider is not None:
+            _data['provider_name'] = provider
+        if share is not None:
+            _data['share_name'] = share
+        return self.client.perform_query('POST', '/unity-catalog/catalogs', data=_data,
+                                         headers=headers)
+
+    def list_catalogs(self, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/catalogs', data=_data,
+                                         headers=headers)
+
+    def get_catalog(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/catalogs/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def update_catalog(self, name, catalog_spec, headers=None):
+        return self.client.perform_query('PATCH', '/unity-catalog/catalogs/%s' % (name),
+                                         data=catalog_spec, headers=headers)
+
+    def delete_catalog(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('DELETE', '/unity-catalog/catalogs/%s' % (name),
+                                         data=_data, headers=headers)
+
+    # Schema Operations
+
+    def create_schema(self, catalog_name, new_schema_name, comment=None, headers=None):
+        _data = {
+            'catalog_name': catalog_name,
+            'name': new_schema_name,
+        }
+        if comment is not None:
+            _data['comment'] = comment
+        return self.client.perform_query('POST', '/unity-catalog/schemas', data=_data,
+                                         headers=headers)
+
+    def list_schemas(self, catalog_name=None, name_regex=None, headers=None):
+        _data = {}
+        if catalog_name is not None:
+            _data['catalog_name'] = catalog_name
+        if name_regex is not None:
+            _data['schema_name_regex'] = name_regex
+
+        return self.client.perform_query('GET', '/unity-catalog/schemas', data=_data,
+                                         headers=headers)
+
+    def get_schema(self, full_name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/schemas/%s' % (full_name),
+                                         data=_data, headers=headers)
+
+    def update_schema(self, full_name, schema_spec, headers=None):
+        return self.client.perform_query('PATCH', '/unity-catalog/schemas/%s' % (full_name),
+                                         data=schema_spec, headers=headers)
+
+    def delete_schema(self, full_name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('DELETE', '/unity-catalog/schemas/%s' % (full_name),
+                                         data=_data, headers=headers)
+
+    # Table Operations
+
+    def create_table(self, table_spec, headers=None):
+        _data = table_spec
+        return self.client.perform_query('POST', '/unity-catalog/tables', data=_data,
+                                         headers=headers)
+
+    def list_tables(self, catalog_name, schema_name=None, name_regex=None, headers=None):
+        _data = {
+            'catalog_name': catalog_name
+        }
+        if schema_name is not None:
+            _data['schema_name'] = schema_name
+        if name_regex is not None:
+            _data['table_name_regex'] = name_regex
+
+        return self.client.perform_query('GET', '/unity-catalog/tables', data=_data,
+                                         headers=headers)
+
+    def list_table_summaries(self, catalog_name, headers=None):
+        _data = {
+            'catalog_name': catalog_name
+        }
+
+        return self.client.perform_query('GET', '/unity-catalog/table-summaries', data=_data,
+                                         headers=headers)
+
+    def get_table(self, full_name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/tables/%s' % (full_name),
+                                         data=_data, headers=headers)
+
+    def update_table(self, full_name, table_spec, headers=None):
+        return self.client.perform_query('PATCH', '/unity-catalog/tables/%s' % (full_name),
+                                         data=table_spec, headers=headers)
+
+    def delete_table(self, full_name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('DELETE', '/unity-catalog/tables/%s' % (full_name),
+                                         data=_data, headers=headers)
+
+    # Share Operations
+
+    def create_share(self, name, headers=None):
+        _data = {
+            'name': name
+        }
+        return self.client.perform_query('POST', '/unity-catalog/shares', data=_data,
+                                         headers=headers)
+
+    def list_shares(self, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/shares', data=_data,
+                                         headers=headers)
+
+    def get_share(self, name, include_shared_data, headers=None):
+        _data = {'include_shared_data': include_shared_data}
+
+        return self.client.perform_query('GET', '/unity-catalog/shares/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def update_share(self, name, share_spec, headers=None):
+        return self.client.perform_query('POST', '/unity-catalog/shares/%s' % (name),
+                                         data=share_spec, headers=headers)
+
+    def delete_share(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('DELETE', '/unity-catalog/shares/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def list_share_permissions(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/shares/%s/permissions' % (name),
+                                         data=_data, headers=headers)
+
+    def update_share_permissions(self, name, perm_spec, headers=None):
+        _data = perm_spec
+
+        return self.client.perform_query('POST', '/unity-catalog/shares/%s/permissions' % (name),
+                                         data=_data, headers=headers)
+
+    # Recipient Operations
+
+    def create_recipient(self, name, comment=None, headers=None):
+        _data = {
+            'name': name,
+        }
+        if comment is not None:
+            _data['comment'] = comment
+        return self.client.perform_query('POST', '/unity-catalog/recipients', data=_data,
+                                         headers=headers)
+
+    def list_recipients(self, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/recipients', data=_data,
+                                         headers=headers)
+
+    def get_recipient(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/recipients/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def rotate_recipient_token(self, name, existing_token_expire_in_seconds=None, headers=None):
+        _data = {
+            'name': name,
+        }
+        if existing_token_expire_in_seconds is not None:
+            _data['existing_token_expire_in_seconds'] = existing_token_expire_in_seconds
+        return self.client.perform_query('POST', '/unity-catalog/recipients/%s/rotate-token' %
+                                         (name), data=_data, headers=headers)
+
+    def get_recipient_share_permissions(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('GET', '/unity-catalog/recipients/%s/share-permissions'
+                                         % (name), data=_data, headers=headers)
+
+    def delete_recipient(self, name, headers=None):
+        _data = {}
+
+        return self.client.perform_query('DELETE', '/unity-catalog/recipients/%s' % (name),
+                                         data=_data, headers=headers)
+
+    # Provider Operations
+
+    def create_provider(self, name, comment, recipient_profile, headers=None):
+        _data = {
+            'name': name,
+            'recipient_profile_str': mc_pretty_format(recipient_profile)
+        }
+        if comment is not None:
+            _data['comment'] = comment
+        return self.client.perform_query('POST', '/unity-catalog/providers/',
+                                         data=_data, headers=headers)
+
+    def list_providers(self, headers=None):
+        return self.client.perform_query('GET', '/unity-catalog/providers', data={},
+                                         headers=headers)
+
+    def get_provider(self, name, headers=None):
+        return self.client.perform_query('GET', '/unity-catalog/providers/%s' % (name),
+                                         data={}, headers=headers)
+
+    def update_provider(self, name, new_name, comment, recipient_profile, headers=None):
+        _data = {}
+        if new_name is not None:
+            _data['name'] = new_name
+        if recipient_profile is not None:
+            _data['recipient_profile'] = recipient_profile
+        if comment is not None:
+            _data['comment'] = comment
+
+        return self.client.perform_query('PATCH', '/unity-catalog/providers/%s' % (name),
+                                         data=_data, headers=headers)
+
+    def delete_provider(self, name, headers=None):
+        return self.client.perform_query('DELETE', '/unity-catalog/providers/%s' % (name),
+                                         data={}, headers=headers)
+
+    def list_provider_shares(self, name, headers=None):
+        return self.client.perform_query('GET', '/unity-catalog/providers/%s/shares' % (name),
+                                         data={}, headers=headers)
+
+    # Permissions Operations
+
+    def _permissions_url(self, sec_type, sec_name):
+        return '/unity-catalog/permissions/%s/%s' % (sec_type, sec_name)
+
+    def get_permissions(self, sec_type, sec_name, headers=None):
+        _data = {}
+        return self.client.perform_query('GET', self._permissions_url(sec_type, sec_name),
+                                         data=_data, headers=headers)
+
+    def update_permissions(self, sec_type, sec_name, perm_diff_spec, headers=None):
+        _data = perm_diff_spec
+        return self.client.perform_query('PATCH', self._permissions_url(sec_type, sec_name),
+                                         data=_data, headers=headers)
+
+    def replace_permissions(self, sec_type, sec_name, perm_spec, headers=None):
+        _data = perm_spec
+        return self.client.perform_query('PUT', self._permissions_url(sec_type, sec_name),
+                                         data=_data, headers=headers)
diff --git a/databricks_cli/unity_catalog/utils.py b/databricks_cli/unity_catalog/utils.py
new file mode 100644
index 00000000..0f88b629
--- /dev/null
+++ b/databricks_cli/unity_catalog/utils.py
@@ -0,0 +1,29 @@
+# Databricks CLI
+# Copyright 2021 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"), except
+# that the use of services to which certain application programming
+# interfaces (each, an "API") connect requires that the user first obtain
+# a license for the use of the APIs from Databricks, Inc. ("Databricks"),
+# by creating an account at www.databricks.com and agreeing to either (a)
+# the Community Edition Terms of Service, (b) the Databricks Terms of
+# Service, or (c) another written agreement between Licensee and Databricks
+# for the use of the APIs.
+#
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from databricks_cli.utils import pretty_format
+
+
+# Encode UTF-8 strings in JSON blobs
+def mc_pretty_format(json):
+    return pretty_format(json, encode_utf8=True)
diff --git a/tools/mkdist.sh b/tools/mkdist.sh
new file mode 100755
index 00000000..29d1a2e8
--- /dev/null
+++ b/tools/mkdist.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+# This program should be run from the databricks-cli directory.
+#
+# It assumes the directory is a 'clean' checkout, as it does not attempt
+# to exclude extraneous files (aside from the contents of the .git/ subdir).
+
+UC_VER="uc10"
+
+# Check that databricks-cli is 'clean'
+if ! git status | grep "nothing to commit, working tree clean"; then
+    echo "PROBLEM: Current directory is not a clean checkout -- remove extraneous files"
+    exit 1
+fi
+
+PY_VERSION="$(python -c 'from databricks_cli.version import version ; print(version)')"
+echo "PY_VERSION = [$PY_VERSION]"
+BASE_VERSION="$(echo $PY_VERSION | sed -e "s/\.dev0//")"
+echo "BASE_VERSION = [$BASE_VERSION]"
+
+WORK_DIR=${PWD}
+echo "WORK_DIR=${WORK_DIR}"
+
+echo "Creating distribution"
+VERSION="${BASE_VERSION}.${UC_VER}"
+echo "VERSION = [$VERSION]"
+
+PKG_NAME="databricks-cli-${VERSION}"
+TARFILE="${PKG_NAME}.tgz"
+
+# Remove the .pyc files that were created when setting the PY_VERSION
+rm databricks_cli/*.pyc
+
+# Fix version.py with new version string
+# Assume running on Mac (sed -i syntax is Mac-specific)
+CMD="sed -i '' 's|${PY_VERSION}|${VERSION}|' databricks_cli/version.py"
+eval $CMD
+
+cd ..
+# Add symlink (since Mac 'tar' doesn't support --transform)
+ln -sf ${WORK_DIR} ${PKG_NAME}
+
+tar -L -czf ${TARFILE} --exclude=.git --exclude=tools ${PKG_NAME}
+
+# Remove symlink
+rm  databricks-cli-${VERSION}
+echo "Distribution created: ../$TARFILE"
+cd $WORK_DIR
+git reset --hard
+echo "Done."
diff --git a/tox.ini b/tox.ini
index e69b44bf..838a9ed3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py36
+envlist = py27, py38
 
 [testenv]
 # https://github.com/codecov/codecov-python/blob/5b9d539a6a09bc84501b381b563956295478651a/README.md#using-tox

From e003ffd4be75cc324a619fd5544b1e58ca2116da Mon Sep 17 00:00:00 2001
From: Adam Cain <adam.cain@databricks.com>
Date: Wed, 29 Dec 2021 18:22:07 -0800
Subject: [PATCH 3/3] Revert py36->py38 change, remove mkdist script

---
 tools/mkdist.sh | 50 -------------------------------------------------
 tox.ini         |  2 +-
 2 files changed, 1 insertion(+), 51 deletions(-)
 delete mode 100755 tools/mkdist.sh

diff --git a/tools/mkdist.sh b/tools/mkdist.sh
deleted file mode 100755
index 29d1a2e8..00000000
--- a/tools/mkdist.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env bash
-
-# This program should be run from the databricks-cli directory.
-#
-# It assumes the directory is a 'clean' checkout, as it does not attempt
-# to exclude extraneous files (aside from the contents of the .git/ subdir).
-
-UC_VER="uc10"
-
-# Check that databricks-cli is 'clean'
-if ! git status | grep "nothing to commit, working tree clean"; then
-    echo "PROBLEM: Current directory is not a clean checkout -- remove extraneous files"
-    exit 1
-fi
-
-PY_VERSION="$(python -c 'from databricks_cli.version import version ; print(version)')"
-echo "PY_VERSION = [$PY_VERSION]"
-BASE_VERSION="$(echo $PY_VERSION | sed -e "s/\.dev0//")"
-echo "BASE_VERSION = [$BASE_VERSION]"
-
-WORK_DIR=${PWD}
-echo "WORK_DIR=${WORK_DIR}"
-
-echo "Creating distribution"
-VERSION="${BASE_VERSION}.${UC_VER}"
-echo "VERSION = [$VERSION]"
-
-PKG_NAME="databricks-cli-${VERSION}"
-TARFILE="${PKG_NAME}.tgz"
-
-# Remove the .pyc files that were created when setting the PY_VERSION
-rm databricks_cli/*.pyc
-
-# Fix version.py with new version string
-# Assume running on Mac (sed -i syntax is Mac-specific)
-CMD="sed -i '' 's|${PY_VERSION}|${VERSION}|' databricks_cli/version.py"
-eval $CMD
-
-cd ..
-# Add symlink (since Mac 'tar' doesn't support --transform)
-ln -sf ${WORK_DIR} ${PKG_NAME}
-
-tar -L -czf ${TARFILE} --exclude=.git --exclude=tools ${PKG_NAME}
-
-# Remove symlink
-rm  databricks-cli-${VERSION}
-echo "Distribution created: ../$TARFILE"
-cd $WORK_DIR
-git reset --hard
-echo "Done."
diff --git a/tox.ini b/tox.ini
index 838a9ed3..e69b44bf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py38
+envlist = py27, py36
 
 [testenv]
 # https://github.com/codecov/codecov-python/blob/5b9d539a6a09bc84501b381b563956295478651a/README.md#using-tox
