diff --git a/extensions/arc/README.md b/extensions/arc/README.md index eea2df9518..ec1fb957eb 100644 --- a/extensions/arc/README.md +++ b/extensions/arc/README.md @@ -2,7 +2,7 @@ Welcome to Microsoft Azure Arc Extension for Azure Data Studio! -**This extension is only applicable to customers in the Azure Arc data services private preview.** +**This extension is only applicable to customers in the Azure Arc data services public preview.** ## Overview diff --git a/extensions/arc/notebooks/arcDataServices/_data/toc.yml b/extensions/arc/notebooks/arcDataServices/_data/toc.yml index 08aa620181..8648d839fb 100644 --- a/extensions/arc/notebooks/arcDataServices/_data/toc.yml +++ b/extensions/arc/notebooks/arcDataServices/_data/toc.yml @@ -8,5 +8,5 @@ not_numbered: true expand_sections: true sections: - - title: TSG100 - The Azure Arc Postgres troubleshooter + - title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter url: postgres/tsg100-troubleshoot-postgres diff --git a/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md b/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md index a3fe0eee38..f374526c4b 100644 --- a/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md +++ b/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md @@ -3,5 +3,5 @@ - This chapter contains notebooks for troubleshooting Postgres on Azure Arc ## Notebooks in this Chapter -- [TSG100 - The Azure Arc Postgres troubleshooter](tsg100-troubleshoot-postgres.ipynb) +- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](tsg100-troubleshoot-postgres.ipynb) diff --git a/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml b/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml index cf63274703..71298e6fc7 100644 --- a/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml +++ b/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml @@ -3,5 +3,5 @@ not_numbered: true expand_sections: true sections: - - title: TSG100 - The Azure Arc Postgres troubleshooter + - title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter url: postgres/tsg100-troubleshoot-postgres diff --git a/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb b/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb index a0ac270ea8..8a7c7af4fb 100644 --- a/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb +++ b/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb @@ -4,13 +4,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "TSG100 - The Azure Arc Postgres troubleshooter\n", - "==============================================\n", + "TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n", + "===================================================================\n", "\n", "Description\n", "-----------\n", "\n", - "Follow these steps to troubleshoot an Azure Arc Postgres Server.\n", + "Follow these steps to troubleshoot an Azure Arc enabled PostgreSQL\n", + "Hyperscale Server.\n", "\n", "Steps\n", "-----\n", @@ -34,6 +35,7 @@ "# the user will be prompted to select a server.\n", "namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n", "name = os.environ.get('POSTGRES_SERVER_NAME')\n", + "version = os.environ.get('POSTGRES_SERVER_VERSION')\n", "\n", "tail_lines = 50" ] @@ -143,7 +145,7 @@ " if cmd.startswith(\"kubectl \") and \"AZDATA_OPENSHIFT\" in os.environ:\n", " cmd_actual[0] = cmd_actual[0].replace(\"kubectl\", \"oc\")\n", "\n", - " # To aid supportabilty, determine which binary file will actually be executed on the machine\n", + " # To aid supportability, determine which binary file will actually be executed on the machine\n", " #\n", " which_binary = None\n", "\n", @@ -400,11 +402,11 @@ "import math\n", "\n", "# If a server was provided, get it\n", - "if namespace and name:\n", - " server = json.loads(run(f'kubectl get dbs -n {namespace} {name} -o json', return_output=True))\n", + "if namespace and name and version:\n", + " server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n", "else:\n", " # Otherwise prompt the user to select a server\n", - " servers = json.loads(run(f'kubectl get dbs --all-namespaces -o json', return_output=True))['items']\n", + " servers = json.loads(run(f'kubectl get postgresqls --all-namespaces -o json', return_output=True))['items']\n", " if not servers:\n", " raise Exception('No Postgres servers found')\n", "\n", @@ -425,6 +427,7 @@ " server = servers[i-1]\n", " namespace = server['metadata']['namespace']\n", " name = server['metadata']['name']\n", + " version = server['kind'][len('postgresql-'):]\n", " break\n", "\n", "display(Markdown(f'#### Got server {namespace}.{name}'))" @@ -446,10 +449,10 @@ "uid = server['metadata']['uid']\n", "\n", "display(Markdown(f'#### Server summary'))\n", - "run(f'kubectl get dbs -n {namespace} {name}')\n", + "run(f'kubectl get postgresql-{version} -n {namespace} {name}')\n", "\n", "display(Markdown(f'#### Resource summary'))\n", - "run(f'kubectl get pods,pvc,svc,ep -n {namespace} -l dusky.microsoft.com/serviceId={uid}')" + "run(f'kubectl get sts,pods,pvc,svc,ep -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')" ] }, { @@ -466,7 +469,7 @@ "outputs": [], "source": [ "display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n", - "run(f'kubectl describe dbs -n {namespace} {name}')" + "run(f'kubectl describe postgresql-{version} -n {namespace} {name}')" ] }, { @@ -482,7 +485,7 @@ "metadata": {}, "outputs": [], "source": [ - "pods = json.loads(run(f'kubectl get pods -n {namespace} -l dusky.microsoft.com/serviceId={uid} -o json', return_output=True))['items']\n", + "pods = json.loads(run(f'kubectl get pods -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid} -o json', return_output=True))['items']\n", "\n", "# Summarize and describe each pod\n", "for pod in pods:\n", @@ -529,8 +532,7 @@ " con_restarts = con_status.get('restartCount', 0)\n", "\n", " display(Markdown(f'#### Troubleshooting container {namespace}.{pod_name}/{con_name} ({i+1}/{len(cons)})\\n'\n", - " f'#### {\"S\" if con_started else \"Not s\"}tarted and '\n", - " f'{\"\" if con_ready else \"not \"}ready with {con_restarts} restarts'))\n", + " f'#### {\"R\" if con_ready else \"Not r\"}eady with {con_restarts} restarts'))\n", "\n", " run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n", "\n", @@ -554,7 +556,7 @@ "outputs": [], "source": [ "display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n", - "run(f'kubectl describe pvc -n {namespace} -l dusky.microsoft.com/serviceId={uid}')" + "run(f'kubectl describe pvc -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')" ] }, { diff --git a/extensions/arc/notebooks/arcDeployment/deploy.arc.control.plane.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb similarity index 86% rename from extensions/arc/notebooks/arcDeployment/deploy.arc.control.plane.ipynb rename to extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb index e24b404cea..a87aac9123 100644 --- a/extensions/arc/notebooks/arcDeployment/deploy.arc.control.plane.ipynb +++ b/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb @@ -47,7 +47,7 @@ "|Tools|Description|Installation|\n", "|---|---|---|\n", "|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n", - "|azdata | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://github.com/microsoft/Azure-data-services-on-Azure-Arc/blob/master/scenarios/001-install-client-tools.md) |" + "|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |" ], "metadata": { "azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471" @@ -90,7 +90,7 @@ "cell_type": "markdown", "source": [ "### **Set variables**\n", - "Generated by Azure Data Studio using the values collected in the Azure Arc Data controller create wizard" + "Generated by Azure Data Studio using the values collected in the 'Create Azure Arc data controller' wizard." ], "metadata": { "azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d" @@ -129,13 +129,11 @@ { "cell_type": "code", "source": [ - "if \"AZDATA_NB_VAR_ARC_DOCKER_PASSWORD\" in os.environ:\n", - " arc_docker_password = os.environ[\"AZDATA_NB_VAR_ARC_DOCKER_PASSWORD\"]\n", "if \"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\" in os.environ:\n", " arc_admin_password = os.environ[\"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\"]\n", "else:\n", " if arc_admin_password == \"\":\n", - " arc_admin_password = getpass.getpass(prompt = 'Azure Arc Data controller password')\n", + " arc_admin_password = getpass.getpass(prompt = 'Azure Arc Data Controller password')\n", " if arc_admin_password == \"\":\n", " sys.exit(f'Password is required.')\n", " confirm_password = getpass.getpass(prompt = 'Confirm password')\n", @@ -175,7 +173,7 @@ { "cell_type": "markdown", "source": [ - "### **Create Azure Arc Data controller**" + "### **Create Azure Arc Data Controller**" ], "metadata": { "azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f" @@ -184,16 +182,14 @@ { "cell_type": "code", "source": [ - "print (f'Creating Azure Arc controller: {arc_data_controller_name} using configuration {arc_cluster_context}')\n", + "print (f'Creating Azure Arc Data Controller: {arc_data_controller_name} using configuration {arc_cluster_context}')\n", "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", "os.environ[\"AZDATA_USERNAME\"] = arc_admin_username\n", "os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\n", - "os.environ[\"DOCKER_USERNAME\"] = arc_docker_username\n", - "os.environ[\"DOCKER_PASSWORD\"] = arc_docker_password\n", "if os.name == 'nt':\n", " print(f'If you don\\'t see output produced by azdata, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n", - "run_command(f'azdata arc dc create --connectivity-mode {arc_data_controller_connectivity_mode} -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n", - "print(f'Azure Arc Data controller cluster: {arc_data_controller_name} created.') " + "run_command(f'azdata arc dc create --connectivity-mode Indirect -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n", + "print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') " ], "metadata": { "azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76", @@ -205,7 +201,7 @@ { "cell_type": "markdown", "source": [ - "### **Setting context to created Azure Arc Data controller**" + "### **Setting context to created Azure Arc Data Controller**" ], "metadata": { "azdata_cell_guid": "a3ddc701-811d-4058-b3fb-b7295fcf50ae" @@ -214,7 +210,7 @@ { "cell_type": "code", "source": [ - "# Setting context to data controller.\n", + "# Setting context to Data Controller.\n", "#\n", "run_command(f'kubectl config set-context --current --namespace {arc_data_controller_namespace}')" ], @@ -227,7 +223,7 @@ { "cell_type": "markdown", "source": [ - "### **Login to the data controller.**\n" + "### **Login to the Data Controller.**\n" ], "metadata": { "azdata_cell_guid": "9376b2ab-0edf-478f-9e3c-5ff46ae3501a" @@ -236,9 +232,9 @@ { "cell_type": "code", "source": [ - "# Login to the data controller.\n", + "# Login to the Data Controller.\n", "#\n", - "run_command(f'azdata login -n {arc_data_controller_namespace}')" + "run_command(f'azdata login --namespace {arc_data_controller_namespace}')" ], "metadata": { "azdata_cell_guid": "9aed0c5a-2c8a-4ad7-becb-60281923a196" @@ -247,4 +243,4 @@ "execution_count": null } ] -} +} \ No newline at end of file diff --git a/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb index 37ae3fabe1..1a31f4a91d 100644 --- a/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb +++ b/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb @@ -25,12 +25,12 @@ "source": [ "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/arc/images/microsoft-small-logo.png)\n", " \n", - "## Deploy a PostgreSQL server group on an existing Azure Arc data cluster\n", + "## Create a PostgreSQL Hyperscale - Azure Arc on an existing Azure Arc Data Controller\n", " \n", - "This notebook walks through the process of deploying a PostgreSQL server group on an existing Azure Arc data cluster.\n", + "This notebook walks through the process of creating a PostgreSQL Hyperscale - Azure Arc on an existing Azure Arc Data Controller.\n", " \n", "* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n", - "* Make sure you have the target Azure Arc data cluster already created.\n", + "* Make sure you have the target Azure Arc Data Controller already created.\n", "\n", "Please press the \"Run All\" button to run the notebook" ], @@ -41,7 +41,21 @@ { "cell_type": "markdown", "source": [ - "### **Check prerequisites**" + "### **Prerequisites** \n", + "Ensure the following tools are installed and added to PATH before proceeding.\n", + " \n", + "|Tools|Description|Installation|\n", + "|---|---|---|\n", + "|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |" + ], + "metadata": { + "azdata_cell_guid": "20fe3985-a01e-461c-bce0-235f7606cc3c" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Setup and Check Prerequisites**" ], "metadata": { "azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7" @@ -75,100 +89,20 @@ { "cell_type": "markdown", "source": [ - "#### **Ensure Postgres Server Group name and password exist**" + "### **Set variables**\n", + "\n", + "#### \n", + "\n", + "Generated by Azure Data Studio using the values collected in the 'Deploy PostgreSQL Hyperscale - Azure Arc instance' wizard" ], "metadata": { "azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb" } }, - { - "cell_type": "code", - "source": [ - "# Required Values\n", - "env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n", - "if env_var:\n", - " controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n", - "if env_var:\n", - " controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n", - "if env_var:\n", - " controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\" in os.environ\n", - "if env_var:\n", - " server_group_name = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\" in os.environ\n", - "if env_var:\n", - " postgres_password = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD was not defined. Exiting\\n') \n", - "\n", - "env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\" in os.environ\n", - "if env_var:\n", - " postgres_storage_class_data = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA was not defined. Exiting\\n') \n", - "env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS\" in os.environ\n", - "if env_var:\n", - " postgres_storage_class_logs = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS was not defined. Exiting\\n') \n", - "env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS\" in os.environ\n", - "if env_var:\n", - " postgres_storage_class_backups = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS was not defined. Exiting\\n') \n", - "" - ], - "metadata": { - "azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b", - "tags": [] - }, - "outputs": [], - "execution_count": null - }, { "cell_type": "markdown", "source": [ - "#### **Get optional parameters for the PostgreSQL server group**" - ], - "metadata": { - "azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb" - } - }, - { - "cell_type": "code", - "source": [ - "server_group_workers = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS\"]\n", - "server_group_port = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT\")\n", - "server_group_cores_request = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST\")\n", - "server_group_cores_limit = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT\")\n", - "server_group_memory_request = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST\")\n", - "server_group_memory_limit = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT\")" - ], - "metadata": { - "azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b", - "tags": [] - }, - "outputs": [], - "execution_count": null - }, - { - "cell_type": "markdown", - "source": [ - "### **Installing PostgreSQL server group**" + "### **Creating the PostgreSQL Hyperscale - Azure Arc instance**" ], "metadata": { "azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189" @@ -179,7 +113,7 @@ "source": [ "# Login to the data controller.\n", "#\n", - "os.environ[\"AZDATA_PASSWORD\"] = controller_password\n", + "os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n", "cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n", "out=run_command()" ], @@ -192,17 +126,22 @@ { "cell_type": "code", "source": [ - "print (f'Creating a PostgreSQL server group on Azure Arc')\n", + "print (f'Creating the PostgreSQL Hyperscale - Azure Arc instance')\n", "\n", - "workers_option = f' -w {server_group_workers}' if server_group_workers else \"\"\n", - "port_option = f' --port \"{server_group_port}\"' if server_group_port else \"\"\n", - "cores_request_option = f' -cr \"{server_group_cores_request}\"' if server_group_cores_request else \"\"\n", - "cores_limit_option = f' -cl \"{server_group_cores_limit}\"' if server_group_cores_limit else \"\"\n", - "memory_request_option = f' -mr \"{server_group_memory_request}Mi\"' if server_group_memory_request else \"\"\n", - "memory_limit_option = f' -ml \"{server_group_memory_limit}Mi\"' if server_group_memory_limit else \"\"\n", + "workers_option = f' -w {postgres_server_group_workers}' if postgres_server_group_workers else \"\"\n", + "port_option = f' --port \"{postgres_server_group_port}\"' if postgres_server_group_port else \"\"\n", + "engine_version_option = f' -ev {postgres_server_group_engine_version}' if postgres_server_group_engine_version else \"\"\n", + "extensions_option = f' --extensions \"{postgres_server_group_extensions}\"' if postgres_server_group_extensions else \"\"\n", + "volume_size_data_option = f' -vsd {postgres_server_group_volume_size_data}Gi' if postgres_server_group_volume_size_data else \"\"\n", + "volume_size_logs_option = f' -vsl {postgres_server_group_volume_size_logs}Gi' if postgres_server_group_volume_size_logs else \"\"\n", + "volume_size_backups_option = f' -vsb {postgres_server_group_volume_size_backups}Gi' if postgres_server_group_volume_size_backups else \"\"\n", + "cores_request_option = f' -cr \"{postgres_server_group_cores_request}\"' if postgres_server_group_cores_request else \"\"\n", + "cores_limit_option = f' -cl \"{postgres_server_group_cores_limit}\"' if postgres_server_group_cores_limit else \"\"\n", + "memory_request_option = f' -mr \"{postgres_server_group_memory_request}Gi\"' if postgres_server_group_memory_request else \"\"\n", + "memory_limit_option = f' -ml \"{postgres_server_group_memory_limit}Gi\"' if postgres_server_group_memory_limit else \"\"\n", "\n", - "os.environ[\"AZDATA_PASSWORD\"] = postgres_password\n", - "cmd = f'azdata arc postgres server create -n {server_group_name} -scd {postgres_storage_class_data} -scl {postgres_storage_class_logs} -scb {postgres_storage_class_backups}{workers_option}{port_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n", + "os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n", + "cmd = f'azdata arc postgres server create -n {postgres_server_group_name} -scd {postgres_storage_class_data} -scl {postgres_storage_class_logs} -scb {postgres_storage_class_backups}{workers_option}{port_option}{engine_version_option}{extensions_option}{volume_size_data_option}{volume_size_logs_option}{volume_size_backups_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n", "out=run_command()" ], "metadata": { diff --git a/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb index b19b8af493..c05627d0ef 100644 --- a/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb +++ b/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb @@ -25,12 +25,12 @@ "source": [ "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/arc/images/microsoft-small-logo.png)\n", " \n", - "## Deploy Azure SQL managed instance on an existing Azure Arc data cluster\n", + "## Create SQL managed instance - Azure Arc on an existing Azure Arc Data Controller\n", " \n", - "This notebook walks through the process of deploying a Azure SQL managed instance on an existing Azure Arc data cluster.\n", + "This notebook walks through the process of creating a SQL managed instance - Azure Arc on an existing Azure Arc Data Controller.\n", " \n", "* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n", - "* Make sure you have the target Azure Arc data cluster already created.\n", + "* Make sure you have the target Azure Arc Data Controller already created.\n", "\n", "Please press the \"Run All\" button to run the notebook" ], @@ -41,7 +41,21 @@ { "cell_type": "markdown", "source": [ - "### **Check prerequisites**" + "### **Prerequisites** \n", + "Ensure the following tools are installed and added to PATH before proceeding.\n", + " \n", + "|Tools|Description|Installation|\n", + "|---|---|---|\n", + "|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |" + ], + "metadata": { + "azdata_cell_guid": "d1c8258e-9efd-4380-a48c-cd675423ed2f" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Setup and Check Prerequisites**" ], "metadata": { "azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7" @@ -75,70 +89,20 @@ { "cell_type": "markdown", "source": [ - "#### **Ensure SQL instance name, username and password exist**" + "### **Set variables**\n", + "\n", + "#### \n", + "\n", + "Generated by Azure Data Studio using the values collected in the 'Deploy Azure SQL managed instance - Azure Arc' wizard" ], "metadata": { "azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb" } }, - { - "cell_type": "code", - "source": [ - "# Required Values\n", - "env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n", - "if env_var:\n", - " controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n", - "if env_var:\n", - " controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n", - "if env_var:\n", - " controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_SQL_INSTANCE_NAME\" in os.environ\n", - "if env_var:\n", - " mssql_instance_name = os.environ[\"AZDATA_NB_VAR_SQL_INSTANCE_NAME\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_INSTANCE_NAME was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_SQL_PASSWORD\" in os.environ\n", - "if env_var:\n", - " mssql_password = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_PASSWORD was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\" in os.environ\n", - "if env_var:\n", - " mssql_storage_class_data = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA was not defined. Exiting\\n')\n", - "\n", - "env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\" in os.environ\n", - "if env_var:\n", - " mssql_storage_class_logs = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\"]\n", - "else:\n", - " sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS was not defined. Exiting\\n') \n", - "" - ], - "metadata": { - "azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b", - "tags": [] - }, - "outputs": [], - "execution_count": null - }, { "cell_type": "markdown", "source": [ - "### **Installing Managed SQL Instance**" + "### **Creating the SQL managed instance - Azure Arc instance**" ], "metadata": { "azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189" @@ -149,7 +113,7 @@ "source": [ "# Login to the data controller.\n", "#\n", - "os.environ[\"AZDATA_PASSWORD\"] = controller_password\n", + "os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n", "cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n", "out=run_command()" ], @@ -162,10 +126,16 @@ { "cell_type": "code", "source": [ - "print (f'Creating Managed SQL Server instance on Azure Arc')\n", + "print (f'Creating the SQL managed instance - Azure Arc instance')\n", "\n", - "os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n", - "cmd = f'azdata arc sql mi create -n {mssql_instance_name} -scd {mssql_storage_class_data} -scl {mssql_storage_class_logs}'\n", + "cores_request_option = f' -cr \"{sql_cores_request}\"' if sql_cores_request else \"\"\n", + "cores_limit_option = f' -cl \"{sql_cores_limit}\"' if sql_cores_limit else \"\"\n", + "memory_request_option = f' -mr \"{sql_memory_request}Gi\"' if sql_memory_request else \"\"\n", + "memory_limit_option = f' -ml \"{sql_memory_limit}Gi\"' if sql_memory_limit else \"\"\n", + "\n", + "os.environ[\"AZDATA_USERNAME\"] = sql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n", + "cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n", "out=run_command()" ], "metadata": { diff --git a/extensions/arc/package.json b/extensions/arc/package.json index c4594cbb4c..3eb07f4a9e 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -2,7 +2,7 @@ "name": "arc", "displayName": "%arc.displayName%", "description": "%arc.description%", - "version": "0.3.5", + "version": "0.5.1", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", @@ -14,6 +14,7 @@ "activationEvents": [ "onCommand:arc.connectToController", "onCommand:arc.createController", + "onCommand:azdata.resource.deploy", "onView:azureArc" ], "extensionDependencies": [ @@ -97,7 +98,7 @@ "view/item/context": [ { "command": "arc.openDashboard", - "when": "view == azureArc && viewItem != postgresInstances", + "when": "view == azureArc && viewItem", "group": "navigation@1" }, { @@ -143,7 +144,7 @@ "providers": [ { "notebookWizard": { - "notebook": "./notebooks/arcDeployment/deploy.arc.control.plane.ipynb", + "notebook": "./notebooks/arcDeployment/deploy.arc.data.controller.ipynb", "type": "new-arc-control-plane", "doneAction": { "label": "%deploy.done.action%" @@ -158,52 +159,30 @@ "generateSummaryPage": false, "pages": [ { - "title": "%arc.control.plane.select.cluster.title%", + "title": "%arc.data.controller.select.cluster.title%", "sections": [ { "fields": [ { "type": "kube_cluster_context_picker", - "label": "%arc.control.plane.kube.cluster.context%", + "label": "%arc.data.controller.kube.cluster.context%", "required": true, "inputWidth": "350px", "variableName": "AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT", "configFileVariableName": "AZDATA_NB_VAR_ARC_CONFIG_FILE" } ] - }, - { - "title": "%arc.control.plane.container.registry.title%", - "fields": [ - { - "label": "%arc.control.plane.container.registry.name%", - "variableName": "AZDATA_NB_VAR_ARC_DOCKER_USERNAME", - "type": "text", - "required": true, - "defaultValue": "22cda7bb-2eb1-419e-a742-8710c313fe79", - "enabled": true - }, - { - "label": "%arc.control.plane.container.registry.password%", - "variableName": "AZDATA_NB_VAR_ARC_DOCKER_PASSWORD", - "type": "password", - "userName": "docker", - "confirmationRequired": false, - "defaultValue": "", - "required": true - } - ] } ] }, { - "title": "%arc.control.plane.cluster.config.profile.title%", + "title": "%arc.data.controller.cluster.config.profile.title%", "sections": [ { "fields": [ { "type": "options", - "label": "%arc.control.plane.cluster.config.profile%", + "label": "%arc.data.controller.cluster.config.profile%", "required": true, "variableName": "AZDATA_NB_VAR_ARC_PROFILE", "editable": false, @@ -220,14 +199,14 @@ ] }, { - "title": "%arc.control.plane.data.controller.create.title%", + "title": "%arc.data.controller.data.controller.create.title%", "sections": [ { - "title": "%arc.control.plane.project.details.title%", + "title": "%arc.data.controller.project.details.title%", "fields": [ { "type": "readonly_text", - "label": "%arc.control.plane.project.details.description%", + "label": "%arc.data.controller.project.details.description%", "labelWidth": "600px" }, { @@ -240,30 +219,30 @@ ] }, { - "title": "%arc.control.plane.data.controller.details.title%", + "title": "%arc.data.controller.data.controller.details.title%", "fields": [ { "type": "readonly_text", - "label": "%arc.control.plane.data.controller.details.description%", + "label": "%arc.data.controller.data.controller.details.description%", "labelWidth": "600px" }, { "type": "text", - "label": "%arc.control.plane.arc.data.controller.namespace%", + "label": "%arc.data.controller.arc.data.controller.namespace%", "textValidationRequired": true, - "textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,11}[a-z0-9])?$", - "textValidationDescription": "%arc.control.plane.arc.data.controller.namespace.validation.description%", + "textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$", + "textValidationDescription": "%arc.data.controller.arc.data.controller.namespace.validation.description%", "defaultValue": "arc", "required": true, "variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE" }, { "type": "text", - "label": "%arc.control.plane.arc.data.controller.name%", + "label": "%arc.data.controller.arc.data.controller.name%", "textValidationRequired": true, - "textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,11}[a-z0-9])?$", - "textValidationDescription": "%arc.control.plane.arc.data.controller.name.validation.description%", - "defaultValue": "arc-cp1", + "textValidationRegex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$", + "textValidationDescription": "%arc.data.controller.arc.data.controller.name.validation.description%", + "defaultValue": "arc-dc", "required": true, "variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME" }, @@ -276,42 +255,33 @@ }, { "type": "azure_locations", - "label": "%arc.control.plane.arc.data.controller.location%", + "label": "%arc.data.controller.arc.data.controller.location%", "defaultValue": "eastus", "required": true, "locationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION", "displayLocationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION", "locations": [ + "australiaeast", + "centralus", "eastus", "eastus2", - "centralus", - "westus2", + "francecentral", + "japaneast", + "koreacentral", + "northeurope", "southeastasia", - "westeurope" + "uksouth", + "westeurope", + "westus2" ] - }, - { - "type": "options", - "label": "%arc.control.plane.arc.data.controller.connectivity.mode%", - "options": { - "values": [ - "Indirect", - "Direct" - ], - "defaultValue": "Indirect", - "optionsType": "radio" - }, - "enabled": false, - "required": true, - "variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE" } ] }, { - "title": "%arc.control.plane.admin.account.title%", + "title": "%arc.data.controller.admin.account.title%", "fields": [ { - "label": "%arc.control.plane.admin.account.name%", + "label": "%arc.data.controller.admin.account.name%", "variableName": "AZDATA_NB_VAR_ARC_ADMIN_USERNAME", "type": "text", "required": true, @@ -319,12 +289,12 @@ "enabled": true }, { - "label": "%arc.control.plane.admin.account.password%", + "label": "%arc.data.controller.admin.account.password%", "variableName": "AZDATA_NB_VAR_ARC_ADMIN_PASSWORD", "type": "sql_password", "userName": "arcadmin", "confirmationRequired": true, - "confirmationLabel": "%arc.control.plane.admin.account.confirm.password%", + "confirmationLabel": "%arc.data.controller.admin.account.confirm.password%", "defaultValue": "", "required": true } @@ -333,7 +303,7 @@ ] }, { - "title": "%arc.control.plane.data.controller.create.summary.title%", + "title": "%arc.data.controller.data.controller.create.summary.title%", "isSummaryPage": true, "fieldHeight": "16px", "sections": [ @@ -349,7 +319,7 @@ { "items": [ { - "label": "%arc.control.plane.summary.arc.data.controller%", + "label": "%arc.data.controller.summary.arc.data.controller%", "type": "readonly_text", "enabled": true, "labelWidth": "185px" @@ -359,7 +329,7 @@ { "items": [ { - "label": "%arc.control.plane.summary.estimated.cost.per.month%", + "label": "%arc.data.controller.summary.estimated.cost.per.month%", "type": "readonly_text", "enabled": true, "labelWidth": "190px", @@ -376,7 +346,7 @@ { "items": [ { - "label": "%arc.control.plane.summary.arc.by.microsoft%", + "label": "%arc.data.controller.summary.arc.by.microsoft%", "type": "readonly_text", "labelWidth": "185px" } @@ -385,7 +355,7 @@ { "items": [ { - "label": "%arc.control.plane.summary.free%", + "label": "%arc.data.controller.summary.free%", "type": "readonly_text", "enabled": true, "defaultValue": "", @@ -403,10 +373,10 @@ "label": "{0}", "type": "readonly_text", "enabled": true, - "labelWidth": "67px", + "labelWidth": "69px", "links": [ { - "text": "%arc.control.plane.summary.arc.terms.of.use%", + "text": "%arc.data.controller.summary.arc.terms.of.use%", "url": "https://go.microsoft.com/fwlink/?linkid=2045708" } ] @@ -423,10 +393,10 @@ "label": "{0}", "type": "readonly_text", "enabled": true, - "labelWidth": "102px", + "labelWidth": "100px", "links": [ { - "text": "%arc.control.plane.summary.arc.terms.privacy.policy%", + "text": "%arc.data.controller.summary.arc.terms.privacy.policy%", "url": "https://go.microsoft.com/fwlink/?linkid=512132" } ] @@ -438,17 +408,17 @@ ] }, { - "title": "%arc.control.plane.summary.terms%", + "title": "%arc.data.controller.summary.terms%", "fieldHeight": "88px", "fields": [ { - "label": "%arc.control.plane.summary.terms.description%", + "label": "%arc.data.controller.summary.terms.description%", "type": "readonly_text", "enabled": true, "labelWidth": "750px", "links": [ { - "text": "%arc.control.plane.summary.terms.link.text%", + "text": "%arc.data.controller.summary.terms.link.text%", "url": "https://go.microsoft.com/fwlink/?linkid=2045624" } ] @@ -456,76 +426,64 @@ ] }, { - "title": "%arc.control.plane.summary.kubernetes%", + "title": "%arc.data.controller.summary.kubernetes%", "fields": [ { - "label": "%arc.control.plane.summary.kube.config.file.path%", + "label": "%arc.data.controller.summary.kube.config.file.path%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_CONFIG_FILE)" }, { - "label": "%arc.control.plane.summary.cluster.context%", + "label": "%arc.data.controller.summary.cluster.context%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT)" }, { - "label": "%arc.control.plane.summary.profile%", + "label": "%arc.data.controller.summary.profile%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_PROFILE)" }, { - "label": "%arc.control.plane.summary.username%", + "label": "%arc.data.controller.summary.username%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_ADMIN_USERNAME)" - }, - { - "label": "%arc.control.plane.summary.docker.username%", - "type": "readonly_text", - "isEvaluated": true, - "defaultValue": "$(AZDATA_NB_VAR_ARC_DOCKER_USERNAME)" } ] }, { - "title": "%arc.control.plane.summary.azure%", + "title": "%arc.data.controller.summary.azure%", "fields": [ { - "label": "%arc.control.plane.summary.data.controller.namespace%", + "label": "%arc.data.controller.summary.data.controller.namespace%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)" }, { - "label": "%arc.control.plane.summary.data.controller.name%", + "label": "%arc.data.controller.summary.data.controller.name%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)" }, { - "label": "%arc.control.plane.summary.data.controller.connectivity.mode%", - "type": "readonly_text", - "isEvaluated": true, - "defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)" - }, - { - "label": "%arc.control.plane.summary.subscription%", + "label": "%arc.data.controller.summary.subscription%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION)", "inputWidth": "600" }, { - "label": "%arc.control.plane.summary.resource.group%", + "label": "%arc.data.controller.summary.resource.group%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_RESOURCE_GROUP)" }, { - "label": "%arc.control.plane.summary.location%", + "label": "%arc.data.controller.summary.location%", "type": "readonly_text", "isEvaluated": true, "defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION)" @@ -542,7 +500,7 @@ }, { "name": "azdata", - "version": "20.1.0" + "version": "20.2.0" } ], "when": true @@ -561,7 +519,7 @@ "tags": ["Hybrid", "SQL Server"], "providers": [ { - "dialog": { + "notebookWizard": { "notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb", "doneAction": { "label": "%deploy.done.action%" @@ -576,14 +534,16 @@ "generateSummaryPage": false, "pages": [ { - "title": "", + "title": "%arc.sql.wizard.page1.title%", + "labelWidth": "175px", + "inputWidth": "280px", "sections": [ { - "title": "%arc.sql.settings.section.title%", + "title": "%arc.sql.connection.settings.section.title%", "fields": [ { "label": "%arc.controller%", - "variableName": "AZDATA_NB_VAR_ARC_CONTROLLER", + "variableName": "", "type": "options", "editable": false, "required": true, @@ -597,23 +557,28 @@ } }, "optionsType": "dropdown" - }, - "labelWidth": "100%" + } }, { "label": "%arc.sql.instance.name%", "variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME", "type": "text", "defaultValue": "sqlinstance1", + "description": "%arc.sql.invalid.instance.name%", "required": true, - "labelWidth": "100%" + "textValidationRequired": true, + "textValidationRegex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$", + "textValidationDescription": "%arc.sql.invalid.instance.name%" }, { "label": "%arc.sql.username%", "variableName": "AZDATA_NB_VAR_SQL_USERNAME", "type": "text", - "defaultValue": "sa", - "enabled": false + "description": "%arc.sql.invalid.username%", + "required": true, + "textValidationRequired": true, + "textValidationRegex": "^(?!sa$)", + "textValidationDescription": "%arc.sql.invalid.username%" }, { "label": "%arc.password%", @@ -624,7 +589,12 @@ "confirmationLabel": "%arc.confirm.password%", "defaultValue": "", "required": true - }, + } + ] + }, + { + "title": "%arc.sql.instance.settings.section.title%", + "fields": [ { "label": "%arc.storage-class.data.label%", "description": "%arc.sql.storage-class.data.description%", @@ -638,6 +608,38 @@ "variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS", "type": "kube_storage_class", "required": true + }, + { + "label": "%arc.cores-request.label%", + "description": "%arc.sql.cores-request.description%", + "variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST", + "type": "number", + "min": 1, + "required": false + }, + { + "label": "%arc.cores-limit.label%", + "description": "%arc.sql.cores-limit.description%", + "variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT", + "type": "number", + "min": 1, + "required": false + }, + { + "label": "%arc.memory-request.label%", + "description": "%arc.sql.memory-request.description%", + "variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST", + "type": "number", + "min": 2, + "required": false + }, + { + "label": "%arc.memory-limit.label%", + "description": "%arc.sql.memory-limit.description%", + "variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT", + "type": "number", + "min": 2, + "required": false } ] } @@ -651,7 +653,7 @@ }, { "name": "azdata", - "version": "20.1.0" + "version": "20.2.0" } ], "when": "true" @@ -683,7 +685,7 @@ "tags": ["Hybrid", "PostgreSQL"], "providers": [ { - "dialog": { + "notebookWizard": { "notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb", "doneAction": { "label": "%deploy.done.action%" @@ -698,14 +700,16 @@ "generateSummaryPage": false, "pages": [ { - "title": "", + "title": "%arc.postgres.wizard.page1.title%", + "labelWidth": "205px", + "inputWidth": "280px", "sections": [ { "title": "%arc.postgres.settings.section.title%", "fields": [ { "label": "%arc.controller%", - "variableName": "AZDATA_NB_VAR_ARC_CONTROLLER", + "variableName": "", "type": "options", "editable": false, "required": true, @@ -719,17 +723,16 @@ } }, "optionsType": "dropdown" - }, - "labelWidth": "100%" + } }, { "label": "%arc.postgres.server.group.name%", "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME", "type": "text", + "description": "%arc.postgres.server.group.name.validation.description%", "textValidationRequired": true, - "textValidationRegex": "^[a-z]([-a-z0-9]{0,8}[a-z0-9])?$", + "textValidationRegex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$", "textValidationDescription": "%arc.postgres.server.group.name.validation.description%", - "defaultValue": "postgres1", "required": true }, { @@ -742,11 +745,12 @@ "required": true }, { - "label": "%arc.postgres.server.group.workers%", + "label": "%arc.postgres.server.group.workers.label%", + "description": "%arc.postgres.server.group.workers.description%", "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS", "type": "number", - "defaultValue": "1", - "min": 1 + "defaultValue": "0", + "min": 0 }, { "label": "%arc.postgres.server.group.port%", @@ -756,6 +760,27 @@ "min": 1, "max": 65535 }, + { + "label": "%arc.postgres.server.group.engine.version%", + "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_ENGINE_VERSION", + "type": "options", + "options": [ + "11", + "12" + ], + "defaultValue": "12" + }, + { + "label": "%arc.postgres.server.group.extensions.label%", + "description": "%arc.postgres.server.group.extensions.description%", + "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_EXTENSIONS", + "type": "text" + } + ] + }, + { + "title": "%arc.postgres.settings.storage.title%", + "fields": [ { "label": "%arc.storage-class.data.label%", "description": "%arc.postgres.storage-class.data.description%", @@ -763,6 +788,14 @@ "type": "kube_storage_class", "required": true }, + { + "label": "%arc.postgres.server.group.volume.size.data.label%", + "description": "%arc.postgres.server.group.volume.size.data.description%", + "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_VOLUME_SIZE_DATA", + "type": "number", + "defaultValue": "5", + "min": 1 + }, { "label": "%arc.storage-class.logs.label%", "description": "%arc.postgres.storage-class.logs.description%", @@ -770,12 +803,28 @@ "type": "kube_storage_class", "required": true }, + { + "label": "%arc.postgres.server.group.volume.size.logs.label%", + "description": "%arc.postgres.server.group.volume.size.logs.description%", + "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_VOLUME_SIZE_LOGS", + "type": "number", + "defaultValue": "5", + "min": 1 + }, { "label": "%arc.storage-class.backups.label%", "description": "%arc.postgres.storage-class.backups.description%", "variableName": "AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS", "type": "kube_storage_class", "required": true + }, + { + "label": "%arc.postgres.server.group.volume.size.backups.label%", + "description": "%arc.postgres.server.group.volume.size.backups.description%", + "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_VOLUME_SIZE_BACKUPS", + "type": "number", + "defaultValue": "5", + "min": 1 } ] }, @@ -783,28 +832,32 @@ "title": "%arc.postgres.settings.resource.title%", "fields": [ { - "label": "%arc.postgres.server.group.cores.request%", + "label": "%arc.postgres.server.group.cores.request.label%", + "description": "%arc.postgres.server.group.cores.request.description%", "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST", "type": "number", - "min": 0 + "min": 1 }, { - "label": "%arc.postgres.server.group.cores.limit%", + "label": "%arc.postgres.server.group.cores.limit.label%", + "description": "%arc.postgres.server.group.cores.limit.description%", "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT", "type": "number", - "min": 0 + "min": 1 }, { - "label": "%arc.postgres.server.group.memory.request%", + "label": "%arc.postgres.server.group.memory.request.label%", + "description": "%arc.postgres.server.group.memory.request.description%", "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST", "type": "number", - "min": 0 + "min": 0.25 }, { - "label": "%arc.postgres.server.group.memory.limit%", + "label": "%arc.postgres.server.group.memory.limit.label%", + "description": "%arc.postgres.server.group.memory.limit.description%", "variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT", "type": "number", - "min": 0 + "min": 0.25 } ] } @@ -814,11 +867,11 @@ }, "requiredTools": [ { - "name": "azure-cli" + "name": "kubectl" }, { "name": "azdata", - "version": "20.1.0" + "version": "20.2.0" } ], "when": "true" @@ -858,5 +911,10 @@ "sinon": "^9.0.2", "typemoq": "2.1.0", "vscodetestcover": "^1.1.0" + }, + "__metadata": { + "id": "68", + "publisherDisplayName": "Microsoft", + "publisherId": "Microsoft" } } diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index dbc75909bf..f86c864342 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -148,15 +148,15 @@ async function promptInputBox(title: string, options: vscode.InputBoxOptions): P } /** - * Opens an input box prompting the user to enter in the name of a resource to delete - * @param name The name of the resource to delete + * Opens an input box prompting the user to enter in the name of an instance to delete + * @param name The name of the instance to delete * @returns Promise resolving to true if the user confirmed the name, false if the input box was closed for any other reason */ -export async function promptForResourceDeletion(name: string): Promise { - const title = loc.resourceDeletionWarning(name); +export async function promptForInstanceDeletion(name: string): Promise { + const title = loc.instanceDeletionWarning(name); const options: vscode.InputBoxOptions = { placeHolder: name, - validateInput: input => input !== name ? loc.invalidResourceDeletionName(name) : '' + validateInput: input => input !== name ? loc.invalidInstanceDeletionName(name) : '' }; return await promptInputBox(title, options) !== undefined; @@ -189,28 +189,15 @@ export async function promptAndConfirmPassword(validate: (input: string) => stri /** * Gets the message to display for a given error object that may be a variety of types. * @param error The error object + * @param useMessageWithLink Whether to use the messageWithLink - if available */ -export function getErrorMessage(error: any): string { +export function getErrorMessage(error: any, useMessageWithLink: boolean = false): string { + if (useMessageWithLink && error.messageWithLink) { + return error.messageWithLink; + } return error.message ?? error; } -/** - * Parses an instance name from the controller. An instance name will either be just its name - * e.g. myinstance or namespace_name e.g. mynamespace_my-instance. - * @param instanceName The instance name in one of the formats described - */ -export function parseInstanceName(instanceName: string | undefined): string { - instanceName = instanceName ?? ''; - const parts: string[] = instanceName.split('_'); - if (parts.length === 2) { - instanceName = parts[1]; - } - else if (parts.length > 2) { - throw new Error(`Cannot parse resource '${instanceName}'. Acceptable formats are 'namespace_name' or 'name'.`); - } - return instanceName; -} - /** * Parses an address into its separate ip and port values. Address must be in the form : * @param address The address to parse diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts index 974138eaea..7522f9373a 100644 --- a/extensions/arc/src/constants.ts +++ b/extensions/arc/src/constants.ts @@ -7,6 +7,11 @@ import * as vscode from 'vscode'; export const refreshActionId = 'arc.refresh'; +export const credentialNamespace = 'arcCredentials'; + +export const controllerTroubleshootDocsUrl = 'https://aka.ms/arc-data-tsg'; +export const miaaTroubleshootDocsUrl = 'https://aka.ms/miaa-tsg'; + export interface IconPath { dark: string; light: string; diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts index a1248aa03f..fccd30eb2f 100644 --- a/extensions/arc/src/extension.ts +++ b/extensions/arc/src/extension.ts @@ -28,6 +28,14 @@ export async function activate(context: vscode.ExtensionContext): Promise { + const nodes = await treeDataProvider.getChildren(); + if (nodes.length > 0) { + const response = await vscode.window.showErrorMessage(loc.onlyOneControllerSupported, loc.yes, loc.no); + if (response !== loc.yes) { + return; + } + await treeDataProvider.removeController(nodes[0] as ControllerTreeNode); + } const dialog = new ConnectToControllerDialog(treeDataProvider); dialog.showDialog(); const model = await dialog.waitForClose(); diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index bf458a6fa0..699309377e 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -8,13 +8,13 @@ import { getErrorMessage } from './common/utils'; const localize = nls.loadMessageBundle(); export const arcDeploymentDeprecation = localize('arc.arcDeploymentDeprecation', "The Arc Deployment extension has been replaced by the Arc extension and has been uninstalled."); -export function arcControllerDashboard(name: string): string { return localize('arc.controllerDashboard', "Azure Arc Controller Dashboard (Preview) - {0}", name); } -export function miaaDashboard(name: string): string { return localize('arc.miaaDashboard', "Managed Instance Dashboard (Preview) - {0}", name); } -export function postgresDashboard(name: string): string { return localize('arc.postgresDashboard', "Postgres Dashboard (Preview) - {0}", name); } +export function arcControllerDashboard(name: string): string { return localize('arc.controllerDashboard', "Azure Arc Data Controller Dashboard (Preview) - {0}", name); } +export function miaaDashboard(name: string): string { return localize('arc.miaaDashboard', "SQL managed instance - Azure Arc Dashboard (Preview) - {0}", name); } +export function postgresDashboard(name: string): string { return localize('arc.postgresDashboard', "PostgreSQL Hyperscale - Azure Arc Dashboard (Preview) - {0}", name); } export const dataControllersType = localize('arc.dataControllersType', "Azure Arc Data Controller"); -export const pgSqlType = localize('arc.pgSqlType', "PostgreSQL Server group - Azure Arc"); -export const miaaType = localize('arc.miaaType', "SQL instance - Azure Arc"); +export const pgSqlType = localize('arc.pgSqlType', "PostgreSQL Hyperscale - Azure Arc"); +export const miaaType = localize('arc.miaaType', "SQL managed instance - Azure Arc"); export const overview = localize('arc.overview', "Overview"); export const connectionStrings = localize('arc.connectionStrings', "Connection Strings"); @@ -72,9 +72,12 @@ export const direct = localize('arc.direct', "Direct"); export const indirect = localize('arc.indirect', "Indirect"); export const loading = localize('arc.loading', "Loading..."); export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials"); +export const noInstancesAvailable = localize('arc.noInstancesAvailable', "No instances available"); export const connectToController = localize('arc.connectToController', "Connect to Existing Controller"); +export function connectToSql(name: string): string { return localize('arc.connectToSql', "Connect to SQL managed instance - Azure Arc ({0})", name); } export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller"); export const controllerUrl = localize('arc.controllerUrl', "Controller URL"); +export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint"); export const controllerName = localize('arc.controllerName', "Name"); export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc"); export const username = localize('arc.username', "Username"); @@ -123,9 +126,11 @@ export const condition = localize('arc.condition', "Condition"); export const details = localize('arc.details', "Details"); export const lastUpdated = localize('arc.lastUpdated', "Last updated"); export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available."); +export const podsReady = localize('arc.podsReady', "pods ready"); export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); } -export function resourceDeleted(name: string): string { return localize('arc.resourceDeleted', "Resource '{0}' deleted", name); } +export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); } +export function instanceDeleted(name: string): string { return localize('arc.instanceDeleted', "Instance '{0}' deleted", name); } export function copiedToClipboard(name: string): string { return localize('arc.copiedToClipboard', "{0} copied to clipboard", name); } export function clickTheTroubleshootButton(resourceType: string): string { return localize('arc.clickTheTroubleshootButton', "Click the troubleshoot button to open the Azure Arc {0} troubleshooting notebook.", resourceType); } export function numVCores(vCores: string | undefined): string { @@ -146,18 +151,19 @@ export const connectionRequired = localize('arc.connectionRequired', "A connecti export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration."); export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); } export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); } -export function resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, getErrorMessage(error)); } +export function instanceDeletionFailed(name: string, error: any): string { return localize('arc.instanceDeletionFailed', "Failed to delete instance {0}. {1}", name, getErrorMessage(error)); } export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); } export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); } +export function connectToSqlFailed(serverName: string, error: any): string { return localize('arc.connectToSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); } export function fetchConfigFailed(name: string, error: any): string { return localize('arc.fetchConfigFailed', "An unexpected error occurred retrieving the config for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchEndpointsFailed(name: string, error: any): string { return localize('arc.fetchEndpointsFailed', "An unexpected error occurred retrieving the endpoints for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); } export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); } -export function resourceDeletionWarning(name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}' type the name '{0}' below to proceed.", name); } -export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); } +export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); } +export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); } export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); } export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); } -export function errorConnectingToController(error: any): string { return localize('arc.errorConnectingToController', "Error connecting to controller. {0}", getErrorMessage(error)); } +export function errorConnectingToController(error: any): string { return localize('arc.errorConnectingToController', "Error connecting to controller. {0}", getErrorMessage(error, true)); } export function passwordAcquisitionFailed(error: any): string { return localize('arc.passwordAcquisitionFailed', "Failed to acquire password. {0}", getErrorMessage(error)); } export const invalidPassword = localize('arc.invalidPassword', "The password did not work, try again."); export function errorVerifyingPassword(error: any): string { return localize('arc.errorVerifyingPassword', "Error encountered while verifying password. {0}", getErrorMessage(error)); } diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts index 34441f6a1f..3ad0a04228 100644 --- a/extensions/arc/src/models/controllerModel.ts +++ b/extensions/arc/src/models/controllerModel.ts @@ -6,7 +6,7 @@ import { ControllerInfo, ResourceType } from 'arc'; import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; -import { parseInstanceName, UserCancelledError } from '../common/utils'; +import { UserCancelledError } from '../common/utils'; import * as loc from '../localizedConstants'; import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; @@ -20,7 +20,6 @@ export type Registration = { export class ControllerModel { private readonly _azdataApi: azdataExt.IExtension; private _endpoints: azdataExt.DcEndpointListResult[] = []; - private _namespace: string = ''; private _registrations: Registration[] = []; private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined; @@ -93,7 +92,7 @@ export class ControllerModel { } public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise { await this.azdataLogin(promptReconnect); - this._registrations = []; + const newRegistrations: Registration[] = []; await Promise.all([ this._azdataApi.azdata.arc.dc.config.show().then(result => { this._controllerConfig = result.result; @@ -125,7 +124,7 @@ export class ControllerModel { }), Promise.all([ this._azdataApi.azdata.arc.postgres.server.list().then(result => { - this._registrations.push(...result.result.map(r => { + newRegistrations.push(...result.result.map(r => { return { instanceName: r.name, state: r.state, @@ -134,7 +133,7 @@ export class ControllerModel { })); }), this._azdataApi.azdata.arc.sql.mi.list().then(result => { - this._registrations.push(...result.result.map(r => { + newRegistrations.push(...result.result.map(r => { return { instanceName: r.name, state: r.state, @@ -143,6 +142,7 @@ export class ControllerModel { })); }) ]).then(() => { + this._registrations = newRegistrations; this.registrationsLastUpdated = new Date(); this._onRegistrationsUpdated.fire(this._registrations); }) @@ -157,10 +157,6 @@ export class ControllerModel { return this._endpoints.find(e => e.name === name); } - public get namespace(): string { - return this._namespace; - } - public get registrations(): Registration[] { return this._registrations; } @@ -171,19 +167,10 @@ export class ControllerModel { public getRegistration(type: ResourceType, name: string): Registration | undefined { return this._registrations.find(r => { - return r.instanceType === type && parseInstanceName(r.instanceName) === name; + return r.instanceType === type && r.instanceName === name; }); } - public async deleteRegistration(_type: ResourceType, _name: string) { - /* TODO chgagnon - if (r && !r.isDeleted && r.customObjectName) { - const r = this.getRegistration(type, name); - await this._registrationRouter.apiV1RegistrationNsNameIsDeletedDelete(this._namespace, r.customObjectName, true); - } - */ - } - /** * property to for use a display label for this controller */ diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index f4383d0aef..46c5c8c852 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -3,13 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceInfo } from 'arc'; +import { MiaaResourceInfo } from 'arc'; import * as azdata from 'azdata'; import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; import { Deferred } from '../common/promise'; -import { UserCancelledError } from '../common/utils'; +import { createCredentialId, parseIpAndPort, UserCancelledError } from '../common/utils'; +import { credentialNamespace } from '../constants'; import * as loc from '../localizedConstants'; +import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; @@ -35,8 +37,8 @@ export class MiaaModel extends ResourceModel { private _refreshPromise: Deferred | undefined = undefined; - constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { - super(info, registration); + constructor(private _controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { + super(_miaaInfo, registration); this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } @@ -155,83 +157,58 @@ export class MiaaModel extends ResourceModel { if (this._connectionProfile) { return; } - let connection: azdata.connection.ConnectionProfile | azdata.connection.Connection | undefined; + const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || ''); + let connectionProfile: azdata.IConnectionProfile | undefined = { + serverName: `${ipAndPort.ip},${ipAndPort.port}`, + databaseName: '', + authenticationType: 'SqlLogin', + providerName: 'MSSQL', + connectionName: '', + userName: this._miaaInfo.userName || '', + password: '', + savePassword: true, + groupFullName: undefined, + saveProfile: true, + id: '', + groupId: undefined, + options: {} + }; + + // If we have the ID stored then try to retrieve the password from previous connections if (this.info.connectionId) { try { - const connections = await azdata.connection.getConnections(); - const existingConnection = connections.find(conn => conn.connectionId === this.info.connectionId); - if (existingConnection) { - const credentials = await azdata.connection.getCredentials(this.info.connectionId); - if (credentials) { - existingConnection.options['password'] = credentials.password; - connection = existingConnection; - } else { - // We need the password so prompt the user for it - const connectionProfile: azdata.IConnectionProfile = { - serverName: existingConnection.options['serverName'], - databaseName: existingConnection.options['databaseName'], - authenticationType: existingConnection.options['authenticationType'], - providerName: 'MSSQL', - connectionName: '', - userName: existingConnection.options['user'], - password: '', - savePassword: false, - groupFullName: undefined, - saveProfile: true, - id: '', - groupId: undefined, - options: existingConnection.options - }; - connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile); + const credentialProvider = await azdata.credentials.getProvider(credentialNamespace); + const credentials = await credentialProvider.readCredential(createCredentialId(this._controllerModel.info.id, this.info.resourceType, this.info.name)); + if (credentials.password) { + // Try to connect to verify credentials are still valid + connectionProfile.password = credentials.password; + // If we don't have a username for some reason then just continue on and we'll prompt for the username below + if (connectionProfile.userName) { + const result = await azdata.connection.connect(connectionProfile, false, false); + if (!result.connected) { + vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage)); + const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this); + connectToSqlDialog.showDialog(connectionProfile); + connectionProfile = await connectToSqlDialog.waitForClose(); + } } } } catch (err) { - // ignore - the connection may not necessarily exist anymore and in that case we'll just reprompt for a connection + console.warn(`Unexpected error fetching password for MIAA instance ${err}`); + // ignore - something happened fetching the password so just reprompt } } - if (!connection) { - // We need the password so prompt the user for it - const connectionProfile: azdata.IConnectionProfile = { - // TODO chgagnon fill in external IP and port - // serverName: (this.registration.externalIp && this.registration.externalPort) ? `${this.registration.externalIp},${this.registration.externalPort}` : '', - serverName: '', - databaseName: '', - authenticationType: 'SqlLogin', - providerName: 'MSSQL', - connectionName: '', - userName: 'sa', - password: '', - savePassword: true, - groupFullName: undefined, - saveProfile: true, - id: '', - groupId: undefined, - options: {} - }; - // Weren't able to load the existing connection so prompt user for new one - connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile); + if (!connectionProfile?.userName || !connectionProfile?.password) { + // Need to prompt user for password since we don't have one stored + const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this); + connectToSqlDialog.showDialog(connectionProfile); + connectionProfile = await connectToSqlDialog.waitForClose(); } - if (connection) { - const profile = { - // The option name might be different here based on where it came from - serverName: connection.options['serverName'] || connection.options['server'], - databaseName: connection.options['databaseName'] || connection.options['database'], - authenticationType: connection.options['authenticationType'], - providerName: 'MSSQL', - connectionName: '', - userName: connection.options['user'], - password: connection.options['password'], - savePassword: false, - groupFullName: undefined, - saveProfile: true, - id: connection.connectionId, - groupId: undefined, - options: connection.options - }; - this.updateConnectionProfile(profile); + if (connectionProfile) { + this.updateConnectionProfile(connectionProfile); } else { throw new UserCancelledError(); } @@ -240,6 +217,7 @@ export class MiaaModel extends ResourceModel { private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { this._connectionProfile = connectionProfile; this.info.connectionId = connectionProfile.id; + this._miaaInfo.userName = connectionProfile.userName; await this._treeDataProvider.saveControllers(); } } diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 4c051a629f..09931200c5 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -4,278 +4,83 @@ *--------------------------------------------------------------------------------------------*/ import { ResourceInfo } from 'arc'; +import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; import * as loc from '../localizedConstants'; -import { Registration } from './controllerModel'; +import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; - -export enum PodRole { - Monitor, - Router, - Shard -} - -export interface V1Pod { - 'apiVersion'?: string; - 'kind'?: string; - 'metadata'?: any; // V1ObjectMeta; - 'spec'?: any; // V1PodSpec; - 'status'?: V1PodStatus; -} - -export interface V1PodStatus { - 'conditions'?: any[]; // Array; - 'containerStatuses'?: Array; - 'ephemeralContainerStatuses'?: any[]; // Array; - 'hostIP'?: string; - 'initContainerStatuses'?: any[]; // Array; - 'message'?: string; - 'nominatedNodeName'?: string; - 'phase'?: string; - 'podIP'?: string; - 'podIPs'?: any[]; // Array; - 'qosClass'?: string; - 'reason'?: string; - 'startTime'?: Date | null; -} - -export interface V1ContainerStatus { - 'containerID'?: string; - 'image'?: string; - 'imageID'?: string; - 'lastState'?: any; // V1ContainerState; - 'name'?: string; - 'ready'?: boolean; - 'restartCount'?: number; - 'started'?: boolean | null; - 'state'?: any; // V1ContainerState; -} - -export interface DuskyObjectModelsDatabaseService { - 'apiVersion'?: string; - 'kind'?: string; - 'metadata'?: any; // V1ObjectMeta; - 'spec'?: any; // DuskyObjectModelsDatabaseServiceSpec; - 'status'?: any; // DuskyObjectModelsDatabaseServiceStatus; - 'arc'?: any; // DuskyObjectModelsDatabaseServiceArcPayload; -} - -export interface V1Status { - 'apiVersion'?: string; - 'code'?: number | null; - 'details'?: any; // V1StatusDetails; - 'kind'?: string; - 'message'?: string; - 'metadata'?: any; // V1ListMeta; - 'reason'?: string; - 'status'?: string; - 'hasObject'?: boolean; -} - -export interface DuskyObjectModelsDatabase { - 'name'?: string; - 'owner'?: string; - 'sharded'?: boolean | null; -} +import { parseIpAndPort } from '../common/utils'; export class PostgresModel extends ResourceModel { - private _service?: DuskyObjectModelsDatabaseService; - private _pods?: V1Pod[]; - private readonly _onServiceUpdated = new vscode.EventEmitter(); - private readonly _onPodsUpdated = new vscode.EventEmitter(); - public onServiceUpdated = this._onServiceUpdated.event; - public onPodsUpdated = this._onPodsUpdated.event; - public serviceLastUpdated?: Date; - public podsLastUpdated?: Date; + private _config?: azdataExt.PostgresServerShowResult; + private readonly _azdataApi: azdataExt.IExtension; - constructor(info: ResourceInfo, registration: Registration) { + private readonly _onConfigUpdated = new vscode.EventEmitter(); + public onConfigUpdated = this._onConfigUpdated.event; + public configLastUpdated?: Date; + + constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) { super(info, registration); + this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } - /** Returns the service's Kubernetes namespace */ - public get namespace(): string | undefined { - return ''; // TODO chgagnon return this.info.namespace; + /** Returns the configuration of Postgres */ + public get config(): azdataExt.PostgresServerShowResult | undefined { + return this._config; } - /** Returns the service's name */ - public get name(): string { - return this.info.name; + /** Returns the major version of Postgres */ + public get engineVersion(): string | undefined { + const kind = this._config?.kind; + return kind + ? kind.substring(kind.lastIndexOf('-') + 1) + : undefined; } - /** Returns the service's fully qualified name in the format namespace.name */ - public get fullName(): string { - return `${this.namespace}.${this.name}`; + /** Returns the IP address and port of Postgres */ + public get endpoint(): { ip: string, port: string } | undefined { + return this._config?.status.externalEndpoint + ? parseIpAndPort(this._config.status.externalEndpoint) + : undefined; } - /** Returns the service's spec */ - public get service(): DuskyObjectModelsDatabaseService | undefined { - return this._service; - } + /** Returns the scale configuration of Postgres e.g. '3 nodes, 1.5 vCores, 1Gi RAM, 2Gi storage per node' */ + public get scaleConfiguration(): string | undefined { + if (!this._config) { + return undefined; + } - /** Returns the service's pods */ - public get pods(): V1Pod[] | undefined { - return this._pods; - } - - /** Refreshes the model */ - public async refresh() { - await Promise.all([ - /* TODO enable - this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name).then(response => { - this._service = response.body; - this.serviceLastUpdated = new Date(); - this._onServiceUpdated.fire(this._service); - }), - this._databaseRouter.getDuskyPods(this.info.namespace || 'test', this.info.name).then(response => { - this._pods = response.body; - this.podsLastUpdated = new Date(); - this._onPodsUpdated.fire(this._pods!); - }) - */ - ]); - } - - /** - * Updates the service - * @param func A function of modifications to apply to the service - */ - public async update(_func: (service: DuskyObjectModelsDatabaseService) => void): Promise { - return undefined; - /* - // Get the latest spec of the service in case it has changed - const service = (await this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body; - service.status = undefined; // can't update the status - func(service); - - return await this._databaseRouter.updateDuskyDatabaseService(this.namespace || 'test', this.name, service).then(r => { - this._service = r.body; - return this._service; - }); - */ - } - - /** Deletes the service */ - public async delete(): Promise { - return undefined; - // return (await this._databaseRouter.deleteDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body; - } - - /** Creates a SQL database in the service */ - public async createDatabase(_db: DuskyObjectModelsDatabase): Promise { - return undefined; - // return (await this._databaseRouter.createDuskyDatabase(this.namespace || 'test', this.name, db)).body; - } - - /** - * Returns the IP address and port of the service, preferring external IP over - * internal IP. If either field is not available it will be set to undefined. - */ - public get endpoint(): { ip?: string, port?: number } { - const externalIp = this._service?.status?.externalIP; - const internalIp = this._service?.status?.internalIP; - const externalPort = this._service?.status?.externalPort; - const internalPort = this._service?.status?.internalPort; - - return externalIp ? { ip: externalIp, port: externalPort ?? undefined } - : internalIp ? { ip: internalIp, port: internalPort ?? undefined } - : { ip: undefined, port: undefined }; - } - - /** Returns the service's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */ - public get configuration(): string { - - // TODO: Resource requests and limits can be configured per role. Figure out how - // to display that in the UI. For now, only show the default configuration. - const cpuLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['cpu']; - const ramLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['memory']; - const cpuRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['cpu']; - const ramRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['memory']; - const storage = this._service?.spec?.storage?.volumeSize; - const nodes = this.pods?.length; + const cpuLimit = this._config.spec.scheduling?.default?.resources?.limits?.cpu; + const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory; + const cpuRequest = this._config.spec.scheduling?.default?.resources?.requests?.cpu; + const ramRequest = this._config.spec.scheduling?.default?.resources?.requests?.memory; + const storage = this._config.spec.storage?.data?.size; + const nodes = (this._config.spec.scale?.shards ?? 0) + 1; // An extra node for the coordinator let configuration: string[] = []; - - if (nodes) { - configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`); - } + configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`); // Prefer limits if they're provided, otherwise use requests if they're provided if (cpuLimit || cpuRequest) { - configuration.push(`${this.formatCores(cpuLimit ?? cpuRequest!)} ${loc.vCores}`); + configuration.push(`${cpuLimit ?? cpuRequest!} ${loc.vCores}`); } if (ramLimit || ramRequest) { - configuration.push(`${this.formatMemory(ramLimit ?? ramRequest!)} ${loc.ram}`); + configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`); } if (storage) { - configuration.push(`${this.formatMemory(storage)} ${loc.storagePerNode}`); + configuration.push(`${storage} ${loc.storagePerNode}`); } return configuration.join(', '); } - /** Given a V1Pod, returns its PodRole or undefined if the role isn't known */ - public static getPodRole(pod: V1Pod): PodRole | undefined { - const name = pod.metadata?.name; - const role = name?.substring(name.lastIndexOf('-'))[1]; - switch (role) { - case 'm': return PodRole.Monitor; - case 'r': return PodRole.Router; - case 's': return PodRole.Shard; - default: return undefined; - } - } - - /** Given a PodRole, returns its localized name */ - public static getPodRoleName(role?: PodRole): string { - switch (role) { - case PodRole.Monitor: return loc.monitor; - case PodRole.Router: return loc.coordinator; - case PodRole.Shard: return loc.worker; - default: return ''; - } - } - - /** Given a V1Pod returns its status */ - public static getPodStatus(pod: V1Pod): string { - const phase = pod.status?.phase; - if (phase !== 'Running') { - return phase ?? ''; - } - - // Pods can be in the running phase while some - // containers are crashing, so check those too. - for (let c of pod.status?.containerStatuses?.filter(c => !c.ready) ?? []) { - const wReason = c.state?.waiting?.reason; - const tReason = c.state?.terminated?.reason; - if (wReason) { return wReason; } - if (tReason) { return tReason; } - } - - return loc.running; - } - - /** - * Converts millicores to cores (600m -> 0.6 cores) - * https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu - * @param cores The millicores to format e.g. 600m - */ - private formatCores(cores: string): number { - return cores?.endsWith('m') ? +cores.slice(0, -1) / 1000 : +cores; - } - - /** - * Formats the memory to end with 'B' e.g: - * 1 -> 1B - * 1K -> 1KB, 1Ki -> 1KiB - * 1M -> 1MB, 1Mi -> 1MiB - * 1G -> 1GB, 1Gi -> 1GiB - * 1T -> 1TB, 1Ti -> 1TiB - * https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory - * @param memory The amount + unit of memory to format e.g. 1K - */ - private formatMemory(memory: string): string { - return memory && !memory.endsWith('B') ? `${memory}B` : memory; + /** Refreshes the model */ + public async refresh() { + await this._controllerModel.azdataLogin(); + this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result; + this.configLastUpdated = new Date(); + this._onConfigUpdated.fire(this._config); } } diff --git a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts index ac1089c09d..2623ab60fa 100644 --- a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts +++ b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts @@ -41,7 +41,9 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro } getVariableValue(variableName: string, controllerLabel: string): Promise { - return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), this.retrieveVariable); + // capture 'this' in an arrow function object + const retrieveVariable = (key: string) => this.retrieveVariable(key); + return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable); } private async getPassword(controller: arc.DataController): Promise { diff --git a/extensions/arc/src/test/common/utils.test.ts b/extensions/arc/src/test/common/utils.test.ts index fde0f65ac2..207f14c96d 100644 --- a/extensions/arc/src/test/common/utils.test.ts +++ b/extensions/arc/src/test/common/utils.test.ts @@ -7,7 +7,7 @@ import { ResourceType } from 'arc'; import 'mocha'; import * as should from 'should'; import * as vscode from 'vscode'; -import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseInstanceName, parseIpAndPort, promptAndConfirmPassword, promptForResourceDeletion, resourceTypeToDisplayName } from '../../common/utils'; +import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseIpAndPort, promptAndConfirmPassword, promptForInstanceDeletion, resourceTypeToDisplayName } from '../../common/utils'; import { ConnectionMode as ConnectionMode, IconPathHelper } from '../../constants'; import * as loc from '../../localizedConstants'; import { MockInputBox } from '../stubs'; @@ -47,24 +47,6 @@ describe('parseEndpoint Method Tests', function (): void { }); }); -describe('parseInstanceName Method Tests', () => { - it('Should parse valid instanceName with namespace correctly', function (): void { - should(parseInstanceName('mynamespace_myinstance')).equal('myinstance'); - }); - - it('Should parse valid instanceName without namespace correctly', function (): void { - should(parseInstanceName('myinstance')).equal('myinstance'); - }); - - it('Should return empty string when undefined value passed in', function (): void { - should(parseInstanceName(undefined)).equal(''); - }); - - it('Should return empty string when empty string value passed in', function (): void { - should(parseInstanceName('')).equal(''); - }); -}); - describe('getAzurecoreApi Method Tests', function () { it('Should get azurecore API correctly', function (): void { should(getAzurecoreApi()).not.be.undefined(); @@ -140,7 +122,7 @@ describe('promptForResourceDeletion Method Tests', function (): void { }); it('Resolves as true when value entered is correct', function (done): void { - promptForResourceDeletion('myname').then((value: boolean) => { + promptForInstanceDeletion('myname').then((value: boolean) => { value ? done() : done(new Error('Expected return value to be true')); }); mockInputBox.value = 'myname'; @@ -148,14 +130,14 @@ describe('promptForResourceDeletion Method Tests', function (): void { }); it('Resolves as false when input box is closed early', function (done): void { - promptForResourceDeletion('myname').then((value: boolean) => { + promptForInstanceDeletion('myname').then((value: boolean) => { !value ? done() : done(new Error('Expected return value to be false')); }); mockInputBox.hide(); }); it('Validation message is set when value entered is incorrect', async function (): Promise { - promptForResourceDeletion('myname'); + promptForInstanceDeletion('myname'); mockInputBox.value = 'wrong value'; await mockInputBox.triggerAccept(); should(mockInputBox.validationMessage).not.be.equal('', 'Validation message should not be empty after incorrect value entered'); @@ -260,22 +242,6 @@ describe('getErrorMessage Method Tests', function () { }); }); -describe('parseInstanceName Method Tests', function () { - it('2 part name', function (): void { - const name = 'MyName'; - should(parseInstanceName(`MyNamespace_${name}`)).equal(name); - }); - - it('1 part name', function (): void { - const name = 'MyName'; - should(parseInstanceName(name)).equal(name); - }); - - it('Invalid name', function (): void { - should(() => parseInstanceName('Some_Invalid_Name')).throwError(); - }); -}); - describe('parseIpAndPort', function (): void { it('Valid address', function (): void { const ip = '127.0.0.1'; diff --git a/extensions/arc/src/test/mocks/fakeAzdataApi.ts b/extensions/arc/src/test/mocks/fakeAzdataApi.ts new file mode 100644 index 0000000000..7312e948f5 --- /dev/null +++ b/extensions/arc/src/test/mocks/fakeAzdataApi.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdataExt from 'azdata-ext'; + +/** + * Simple fake Azdata Api used to mock the API during tests + */ +export class FakeAzdataApi implements azdataExt.IAzdataApi { + + public postgresInstances: azdataExt.PostgresServerListResult[] = []; + public miaaInstances: azdataExt.SqlMiListResult[] = []; + + // + // API Implementation + // + public get arc() { + const self = this; + return { + dc: { + create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise> { throw new Error('Method not implemented.'); }, + endpoint: { + async list(): Promise> { return { result: [] }; } + }, + config: { + list(): Promise> { throw new Error('Method not implemented.'); }, + async show(): Promise> { return { result: undefined! }; } + } + }, + postgres: { + server: { + delete(_name: string): Promise> { throw new Error('Method not implemented.'); }, + async list(): Promise> { return { result: self.postgresInstances }; }, + show(_name: string): Promise> { throw new Error('Method not implemented.'); }, + edit( + _name: string, + _args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + _additionalEnvVars?: { [key: string]: string }): Promise> { throw new Error('Method not implemented.'); } + } + }, + sql: { + mi: { + delete(_name: string): Promise> { throw new Error('Method not implemented.'); }, + async list(): Promise> { return { result: self.miaaInstances }; }, + show(_name: string): Promise> { throw new Error('Method not implemented.'); } + } + } + }; + } + getPath(): Promise { + throw new Error('Method not implemented.'); + } + login(_endpoint: string, _username: string, _password: string): Promise> { + return undefined; + } + version(): Promise> { + throw new Error('Method not implemented.'); + } + getSemVersion(): any { + throw new Error('Method not implemented.'); + } + +} diff --git a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts index a6161223f0..3d429de41a 100644 --- a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts +++ b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts @@ -3,16 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ControllerInfo } from 'arc'; +import { ControllerInfo, ResourceType } from 'arc'; import 'mocha'; import * as should from 'should'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { v4 as uuid } from 'uuid'; import * as vscode from 'vscode'; +import * as azdataExt from 'azdata-ext'; import { ControllerModel } from '../../../models/controllerModel'; +import { MiaaModel } from '../../../models/miaaModel'; import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider'; import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode'; +import { MiaaTreeNode } from '../../../ui/tree/miaaTreeNode'; import { FakeControllerModel } from '../../mocks/fakeControllerModel'; +import { FakeAzdataApi } from '../../mocks/fakeAzdataApi'; describe('AzureArcTreeDataProvider tests', function (): void { let treeDataProvider: AzureArcTreeDataProvider; @@ -84,6 +89,27 @@ describe('AzureArcTreeDataProvider tests', function (): void { let children = await treeDataProvider.getChildren(); should(children.length).equal(0, 'After loading we should have 0 children'); }); + + it('should return all children of controller after loading', async function (): Promise { + const mockArcExtension = TypeMoq.Mock.ofType>(); + const mockArcApi = TypeMoq.Mock.ofType(); + mockArcExtension.setup(x => x.exports).returns(() => { + return mockArcApi.object; + }); + const fakeAzdataApi = new FakeAzdataApi(); + fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }]; + fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }]; + mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi); + + sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object); + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword'); + await treeDataProvider.addOrUpdateController(controllerModel, ''); + const controllerNode = treeDataProvider.getControllerNode(controllerModel); + const children = await treeDataProvider.getChildren(controllerNode); + should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child'); + should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child'); + should(children.length).equal(2, 'Should have excatly 2 children'); + }); }); describe('removeController', function (): void { @@ -104,4 +130,31 @@ describe('AzureArcTreeDataProvider tests', function (): void { should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node again should do nothing'); }); }); + + describe('openResourceDashboard', function (): void { + it('Opening dashboard for nonexistent controller node throws', async function (): Promise { + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); + await should(openDashboardPromise).be.rejected(); + }); + + it('Opening dashboard for nonexistent resource throws', async function (): Promise { + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + await treeDataProvider.addOrUpdateController(controllerModel, ''); + const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); + await should(openDashboardPromise).be.rejected(); + }); + + it('Opening dashboard for existing resource node succeeds', async function (): Promise { + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider); + await treeDataProvider.addOrUpdateController(controllerModel, ''); + const controllerNode = treeDataProvider.getControllerNode(controllerModel)!; + const resourceNode = new MiaaTreeNode(miaaModel, controllerModel); + sinon.stub(controllerNode, 'getResourceNode').returns(resourceNode); + const showDashboardStub = sinon.stub(resourceNode, 'openDashboard'); + await treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); + should(showDashboardStub.calledOnce).be.true('showDashboard should have been called exactly once'); + }); + }); }); diff --git a/extensions/arc/src/typings/arc.d.ts b/extensions/arc/src/typings/arc.d.ts index 8784c2ac54..0fcc14c352 100644 --- a/extensions/arc/src/typings/arc.d.ts +++ b/extensions/arc/src/typings/arc.d.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ declare module 'arc' { - import * as vscode from 'vscode'; /** * Covers defining what the arc extension exports to other extensions @@ -20,6 +19,10 @@ declare module 'arc' { sqlManagedInstances = 'sqlManagedInstances' } + export type MiaaResourceInfo = ResourceInfo & { + userName?: string + }; + export type ResourceInfo = { name: string, resourceType: ResourceType | string, diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts index 13d53c3d81..a917e26adc 100644 --- a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts @@ -7,8 +7,8 @@ import { ResourceType } from 'arc'; import * as azdata from 'azdata'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; -import { getConnectionModeDisplayText, getResourceTypeIcon, parseInstanceName, resourceTypeToDisplayName } from '../../../common/utils'; -import { cssStyles, Endpoints, IconPathHelper, iconSize } from '../../../constants'; +import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils'; +import { cssStyles, Endpoints, IconPathHelper, controllerTroubleshootDocsUrl, iconSize } from '../../../constants'; import * as loc from '../../../localizedConstants'; import { ControllerModel } from '../../../models/controllerModel'; import { DashboardPage } from '../../components/dashboardPage'; @@ -93,7 +93,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage { headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }, { - displayName: loc.compute, + displayName: loc.state, valueType: azdata.DeclarativeDataType.string, width: '34%', isReadOnly: true, @@ -178,18 +178,30 @@ export class ControllerDashboardOverviewPage extends DashboardPage { this._openInAzurePortalButton.onDidClick(async () => { const config = this._controllerModel.controllerConfig; if (config) { - vscode.env.openExternal(vscode.Uri.parse( + await vscode.env.openExternal(vscode.Uri.parse( `https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.dataControllers}/${config.metadata.name}`)); } else { vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration); } })); + const troubleshootButton = this.modelView.modelBuilder.button().withProperties({ + label: loc.troubleshoot, + iconPath: IconPathHelper.wrench + }).component(); + + this.disposables.push( + troubleshootButton.onDidClick(async () => { + await vscode.env.openExternal(vscode.Uri.parse(controllerTroubleshootDocsUrl)); + }) + ); + return this.modelView.modelBuilder.toolbarContainer().withToolbarItems( [ { component: newInstance }, { component: refreshButton, toolbarSeparatorAfter: true }, - { component: this._openInAzurePortalButton } + { component: this._openInAzurePortalButton, toolbarSeparatorAfter: true }, + { component: troubleshootButton } ] ).component(); } @@ -219,26 +231,18 @@ export class ControllerDashboardOverviewPage extends DashboardPage { iconHeight: iconSize, iconWidth: iconSize }).component(); - let nameComponent: azdata.Component; - if (r.instanceType === ResourceType.postgresInstances) { - nameComponent = this.modelView.modelBuilder.text() - .withProperties({ - value: r.instanceName || '', - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); - } else { - nameComponent = this.modelView.modelBuilder.hyperlink() - .withProperties({ - label: r.instanceName || '', - url: '' - }).component(); - (nameComponent).onDidClick(async () => { - await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', parseInstanceName(r.instanceName)); - }); - } - // TODO chgagnon - return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), '-'/* loc.numVCores(r.vCores) */]; + const nameComponent = this.modelView.modelBuilder.hyperlink() + .withProperties({ + label: r.instanceName || '', + url: '' + }).component(); + + this.disposables.push(nameComponent.onDidClick(async () => { + await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName); + })); + + return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), r.state]; }); this._arcResourcesLoadingComponent.loading = !this._controllerModel.registrationsLastUpdated; } diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts index 0beddaeb1d..4bf3ad19ae 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts @@ -7,8 +7,8 @@ import * as azdata from 'azdata'; import * as azdataExt from 'azdata-ext'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; -import { getDatabaseStateDisplayText, promptForResourceDeletion } from '../../../common/utils'; -import { cssStyles, Endpoints, IconPathHelper } from '../../../constants'; +import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils'; +import { cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants'; import * as loc from '../../../localizedConstants'; import { ControllerModel } from '../../../models/controllerModel'; import { MiaaModel } from '../../../models/miaaModel'; @@ -198,13 +198,22 @@ export class MiaaDashboardOverviewPage extends DashboardPage { deleteButton.onDidClick(async () => { deleteButton.enabled = false; try { - if (await promptForResourceDeletion(this._miaaModel.info.name)) { - await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name); + if (await promptForInstanceDeletion(this._miaaModel.info.name)) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.deletingInstance(this._miaaModel.info.name), + cancellable: false + }, + (_progress, _token) => { + return this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name); + } + ); await this._controllerModel.refreshTreeNode(); - vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.info.name)); + vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name)); } } catch (error) { - vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.info.name, error)); + vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error)); } finally { deleteButton.enabled = true; } @@ -248,6 +257,17 @@ export class MiaaDashboardOverviewPage extends DashboardPage { } })); + const troubleshootButton = this.modelView.modelBuilder.button().withProperties({ + label: loc.troubleshoot, + iconPath: IconPathHelper.wrench + }).component(); + + this.disposables.push( + troubleshootButton.onDidClick(async () => { + await vscode.env.openExternal(vscode.Uri.parse(miaaTroubleshootDocsUrl)); + }) + ); + return this.modelView.modelBuilder.toolbarContainer().withToolbarItems( [ { component: deleteButton }, @@ -332,19 +352,13 @@ export class MiaaDashboardOverviewPage extends DashboardPage { } private refreshDashboardLinks(): void { - const kibanaEndpoint = this._controllerModel.getEndpoint(Endpoints.logsui); - if (kibanaEndpoint && this._miaaModel.config) { - const kibanaQuery = `kubernetes_namespace:"${this._miaaModel.config.metadata.namespace}" and custom_resource_name :"${this._miaaModel.config.metadata.name}"`; - const kibanaUrl = `${kibanaEndpoint.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`; + if (this._miaaModel.config) { + const kibanaUrl = this._miaaModel.config.status.logSearchDashboard ?? ''; this._kibanaLink.label = kibanaUrl; this._kibanaLink.url = kibanaUrl; this._kibanaLoading!.loading = false; - } - const grafanaEndpoint = this._controllerModel.getEndpoint(Endpoints.metricsui); - if (grafanaEndpoint && this._miaaModel.config) { - const grafanaQuery = `var-hostname=${this._miaaModel.info.name}-0`; - const grafanaUrl = grafanaEndpoint ? `${grafanaEndpoint.endpoint}/d/40q72HnGk/sql-managed-instance-metrics?${grafanaQuery}` : ''; + const grafanaUrl = this._miaaModel.config.status.metricsDashboard ?? ''; this._grafanaLink.label = grafanaUrl; this._grafanaLink.url = grafanaUrl; this._grafanaLoading!.loading = false; diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts deleted file mode 100644 index e0cccd65df..0000000000 --- a/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as azdata from 'azdata'; -import * as loc from '../../../localizedConstants'; -import { IconPathHelper } from '../../../constants'; -import { DashboardPage } from '../../components/dashboardPage'; - -export class PostgresBackupPage extends DashboardPage { - protected get title(): string { - return loc.backup; - } - - protected get id(): string { - return 'postgres-backup'; - } - - protected get icon(): { dark: string; light: string; } { - return IconPathHelper.backup; - } - - protected get container(): azdata.Component { - return this.modelView.modelBuilder.text().withProperties({ value: loc.backup }).component(); - } - - protected get toolbarContainer(): azdata.ToolbarContainer { - return this.modelView.modelBuilder.toolbarContainer().component(); - } -} diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts deleted file mode 100644 index f01148cb3d..0000000000 --- a/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as azdata from 'azdata'; -import * as loc from '../../../localizedConstants'; -import { IconPathHelper } from '../../../constants'; -import { DashboardPage } from '../../components/dashboardPage'; - -export class PostgresComputeStoragePage extends DashboardPage { - protected get title(): string { - return loc.computeAndStorage; - } - - protected get id(): string { - return 'postgres-compute-storage'; - } - - protected get icon(): { dark: string; light: string; } { - return IconPathHelper.computeStorage; - } - - protected get container(): azdata.Component { - return this.modelView.modelBuilder.text().withProperties({ value: loc.computeAndStorage }).component(); - } - - protected get toolbarContainer(): azdata.ToolbarContainer { - return this.modelView.modelBuilder.toolbarContainer().component(); - } -} diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 50857543eb..183fe123bb 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { IconPathHelper, cssStyles } from '../../../constants'; @@ -12,13 +11,12 @@ import { DashboardPage } from '../../components/dashboardPage'; import { PostgresModel } from '../../../models/postgresModel'; export class PostgresConnectionStringsPage extends DashboardPage { - private loading?: azdata.LoadingComponent; private keyValueContainer?: KeyValueContainer; constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { super(modelView); - this.disposables.push(this._postgresModel.onServiceUpdated( + this.disposables.push(this._postgresModel.onConfigUpdated( () => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated()))); } @@ -61,44 +59,20 @@ export class PostgresConnectionStringsPage extends DashboardPage { this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings()); this.disposables.push(this.keyValueContainer); - - this.loading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.keyValueContainer.container) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated - }).component(); - - content.addItem(this.loading); + content.addItem(this.keyValueContainer.container); this.initialized = true; return root; } protected get toolbarContainer(): azdata.ToolbarContainer { - const refreshButton = this.modelView.modelBuilder.button().withProperties({ - label: loc.refresh, - iconPath: IconPathHelper.refresh - }).component(); - - this.disposables.push( - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - this.loading!.loading = true; - await this._postgresModel.refresh(); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } finally { - refreshButton.enabled = true; - } - })); - - return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ - { component: refreshButton } - ]).component(); + return this.modelView.modelBuilder.toolbarContainer().component(); } private getConnectionStrings(): KeyValue[] { - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; + const endpoint = this._postgresModel.endpoint; + if (!endpoint) { + return []; + } return [ new InputKeyValue(this.modelView.modelBuilder, 'ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`), @@ -115,6 +89,5 @@ export class PostgresConnectionStringsPage extends DashboardPage { private handleServiceUpdated() { this.keyValueContainer?.refresh(this.getConnectionStrings()); - this.loading!.loading = false; } } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts index 3c256e3d15..b63d6218ce 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts @@ -10,15 +10,13 @@ import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; import { PostgresOverviewPage } from './postgresOverviewPage'; import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage'; -import { PostgresPropertiesPage } from './postgresPropertiesPage'; import { Dashboard } from '../../components/dashboard'; import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage'; import { PostgresSupportRequestPage } from './postgresSupportRequestPage'; -import { PostgresResourceHealthPage } from './postgresResourceHealthPage'; export class PostgresDashboard extends Dashboard { constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { - super(loc.postgresDashboard(_postgresModel.name), 'ArcPgDashboard'); + super(loc.postgresDashboard(_postgresModel.info.name), 'ArcPgDashboard'); } public async showDashboard(): Promise { @@ -32,24 +30,22 @@ export class PostgresDashboard extends Dashboard { protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> { const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel); const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel); - const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel); - const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel); + // TODO: Removed properties page while investigating bug where refreshed values don't appear in UI + // const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel); const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel); - const supportRequestPage = new PostgresSupportRequestPage(modelView); + const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel); return [ overviewPage.tab, { title: loc.settings, tabs: [ - connectionStringsPage.tab, - propertiesPage.tab + connectionStringsPage.tab ] }, { title: loc.supportAndTroubleshooting, tabs: [ - resourceHealthPage.tab, diagnoseAndSolveProblemsPage.tab, supportRequestPage.tab ] diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts index bb884d3b2b..cc94746b9b 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts @@ -50,8 +50,9 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { this.disposables.push( troubleshootButton.onDidClick(() => { - process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace; - process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name; + process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace; + process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name; + process.env['POSTGRES_SERVER_VERSION'] = this._postgresModel.engineVersion; vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); })); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts deleted file mode 100644 index ae91757a42..0000000000 --- a/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as azdata from 'azdata'; -import * as loc from '../../../localizedConstants'; -import { IconPathHelper } from '../../../constants'; -import { DashboardPage } from '../../components/dashboardPage'; - -export class PostgresNetworkingPage extends DashboardPage { - protected get title(): string { - return loc.networking; - } - - protected get id(): string { - return 'postgres-networking'; - } - - protected get icon(): { dark: string; light: string; } { - return IconPathHelper.networking; - } - - protected get container(): azdata.Component { - return this.modelView.modelBuilder.text().withProperties({ value: loc.networking }).component(); - } - - protected get toolbarContainer(): azdata.ToolbarContainer { - return this.modelView.modelBuilder.toolbarContainer().component(); - } -} diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index a3d501cc73..d293640215 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -5,33 +5,34 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; +import * as azdataExt from 'azdata-ext'; import * as loc from '../../../localizedConstants'; -import { IconPathHelper, cssStyles, Endpoints } from '../../../constants'; +import { IconPathHelper, cssStyles } from '../../../constants'; import { DashboardPage } from '../../components/dashboardPage'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; -import { promptAndConfirmPassword } from '../../../common/utils'; +import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils'; +import { ResourceType } from 'arc'; export class PostgresOverviewPage extends DashboardPage { - private propertiesLoading?: azdata.LoadingComponent; - private kibanaLoading?: azdata.LoadingComponent; - private grafanaLoading?: azdata.LoadingComponent; - private nodesTableLoading?: azdata.LoadingComponent; + private propertiesLoading!: azdata.LoadingComponent; + private kibanaLoading!: azdata.LoadingComponent; + private grafanaLoading!: azdata.LoadingComponent; - private properties?: azdata.PropertiesContainerComponent; - private kibanaLink?: azdata.HyperlinkComponent; - private grafanaLink?: azdata.HyperlinkComponent; - private nodesTable?: azdata.DeclarativeTableComponent; + private properties!: azdata.PropertiesContainerComponent; + private kibanaLink!: azdata.HyperlinkComponent; + private grafanaLink!: azdata.HyperlinkComponent; + + private readonly _azdataApi: azdataExt.IExtension; constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); + this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; this.disposables.push( - this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated())), this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())), - this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())), - this._postgresModel.onPodsUpdated(() => this.eventuallyRunOnInitialized(() => this.handlePodsUpdated()))); + this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated()))); } protected get title(): string { @@ -60,7 +61,7 @@ export class PostgresOverviewPage extends DashboardPage { this.propertiesLoading = this.modelView.modelBuilder.loadingComponent() .withItem(this.properties) .withProperties({ - loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated + loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.configLastUpdated }).component(); content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text }); @@ -72,29 +73,26 @@ export class PostgresOverviewPage extends DashboardPage { CSSStyles: titleCSS }).component()); - this.kibanaLink = this.modelView.modelBuilder.hyperlink() - .withProperties({ - label: this.getKibanaLink(), - url: this.getKibanaLink() - }).component(); + this.kibanaLink = this.modelView.modelBuilder.hyperlink().component(); - this.grafanaLink = this.modelView.modelBuilder.hyperlink() - .withProperties({ - label: this.getGrafanaLink(), - url: this.getGrafanaLink() - }).component(); + this.grafanaLink = this.modelView.modelBuilder.hyperlink().component(); this.kibanaLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.kibanaLink) - .withProperties({ - loading: !this._controllerModel.endpointsLastUpdated - }).component(); + .withProperties( + { loading: !this._postgresModel?.configLastUpdated } + ) + .component(); this.grafanaLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.grafanaLink) - .withProperties({ - loading: !this._controllerModel.endpointsLastUpdated - }).component(); + .withProperties( + { loading: !this._postgresModel?.configLastUpdated } + ) + .component(); + + this.refreshDashboardLinks(); + + this.kibanaLoading.component = this.kibanaLink; + this.grafanaLoading.component = this.grafanaLink; const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ width: '100%', @@ -134,60 +132,8 @@ export class PostgresOverviewPage extends DashboardPage { [loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription], [loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]] }).component(); + content.addItem(endpointsTable); - - // Server group nodes - content.addItem(this.modelView.modelBuilder.text().withProperties({ - value: loc.serverGroupNodes, - CSSStyles: titleCSS - }).component()); - - this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - width: '100%', - columns: [ - { - displayName: loc.name, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '30%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.type, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '15%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.status, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '20%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.fullyQualifiedDomain, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '35%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - } - ], - data: this.getNodes() - }).component(); - - this.nodesTableLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.nodesTable) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated - }).component(); - - content.addItem(this.nodesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } }); this.initialized = true; return root; } @@ -205,11 +151,13 @@ export class PostgresOverviewPage extends DashboardPage { try { const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : ''); if (password) { - await this._postgresModel.update(s => { - // TODO chgagnon - // s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload(); - s.arc.servicePassword = password; - }); + await this._azdataApi.azdata.arc.postgres.server.edit( + this._postgresModel.info.name, + { + adminPassword: true, + noWait: true + }, + { 'AZDATA_PASSWORD': password }); vscode.window.showInformationMessage(loc.passwordReset); } } catch (error) { @@ -229,15 +177,22 @@ export class PostgresOverviewPage extends DashboardPage { deleteButton.onDidClick(async () => { deleteButton.enabled = false; try { - /* - if (await promptForResourceDeletion(this._postgresModel.namespace, this._postgresModel.name)) { - await this._postgresModel.delete(); - await this._controllerModel.deleteRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName)); + if (await promptForInstanceDeletion(this._postgresModel.info.name)) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: loc.deletingInstance(this._postgresModel.info.name), + cancellable: false + }, + (_progress, _token) => { + return this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name); + } + ); + await this._controllerModel.refreshTreeNode(); + vscode.window.showInformationMessage(loc.instanceDeleted(this._postgresModel.info.name)); } - */ } catch (error) { - vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error)); + vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error)); } finally { deleteButton.enabled = true; } @@ -256,7 +211,6 @@ export class PostgresOverviewPage extends DashboardPage { this.propertiesLoading!.loading = true; this.kibanaLoading!.loading = true; this.grafanaLoading!.loading = true; - this.nodesTableLoading!.loading = true; await Promise.all([ this._postgresModel.refresh(), @@ -278,15 +232,13 @@ export class PostgresOverviewPage extends DashboardPage { this.disposables.push( openInAzurePortalButton.onDidClick(async () => { - /* - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); - } else { + const azure = this._controllerModel.controllerConfig?.spec.settings.azure; + if (azure) { vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`)); + `https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}`)); + } else { + vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration); } - */ })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ @@ -298,63 +250,35 @@ export class PostgresOverviewPage extends DashboardPage { } private getProperties(): azdata.PropertiesContainerItem[] { - /* - const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; + const status = this._postgresModel.config?.status; + const azure = this._controllerModel.controllerConfig?.spec.settings.azure; return [ - { displayName: loc.name, value: this._postgresModel.name }, - { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres@${endpoint.ip}:${endpoint.port}` }, - { displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' }, + { displayName: loc.resourceGroup, value: azure?.resourceGroup || '-' }, + { displayName: loc.dataController, value: this._controllerModel.controllerConfig?.metadata.name || '-' }, + { displayName: loc.region, value: azure?.location || '-' }, + { displayName: loc.namespace, value: this._postgresModel.config?.metadata.namespace || '-' }, + { displayName: loc.subscriptionId, value: azure?.subscription || '-' }, + { displayName: loc.externalEndpoint, value: this._postgresModel.config?.status.externalEndpoint || '-' }, + { displayName: loc.status, value: status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : '-' }, { displayName: loc.postgresAdminUsername, value: 'postgres' }, - { displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' }, - { displayName: loc.nodeConfiguration, value: this._postgresModel.configuration }, - { displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' }, - { displayName: loc.postgresVersion, value: this._postgresModel.service?.spec?.engine?.version?.toString() ?? '' } + { displayName: loc.postgresVersion, value: this._postgresModel.engineVersion ?? '-' }, + { displayName: loc.nodeConfiguration, value: this._postgresModel.scaleConfiguration || '-' } ]; - */ - return []; } - private getKibanaLink(): string { - const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace}" and custom_resource_name:"${this._postgresModel.name}"`; - return `${this._controllerModel.getEndpoint(Endpoints.logsui)?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`; + private refreshDashboardLinks(): void { + if (this._postgresModel.config) { + const kibanaUrl = this._postgresModel.config.status.logSearchDashboard ?? ''; + this.kibanaLink.label = kibanaUrl; + this.kibanaLink.url = kibanaUrl; + this.kibanaLoading.loading = false; - } - - private getGrafanaLink(): string { - const grafanaQuery = `var-Namespace=${this._postgresModel.namespace}&var-Name=${this._postgresModel.name}`; - return `${this._controllerModel.getEndpoint(Endpoints.metricsui)?.endpoint}/d/postgres-metrics?${grafanaQuery}`; - } - - private getNodes(): string[][] { - /* TODO chgagnon - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; - return this._postgresModel.pods?.map((pod: V1Pod) => { - const name = pod.metadata?.name ?? ''; - const role: PodRole | undefined = PostgresModel.getPodRole(pod); - const service = pod.metadata?.annotations?.['arcdata.microsoft.com/serviceHost']; - const internalDns = service ? `${name}.${service}` : ''; - - return [ - name, - PostgresModel.getPodRoleName(role), - PostgresModel.getPodStatus(pod), - role === PodRole.Router ? `${endpoint.ip}:${endpoint.port}` : internalDns - ]; - }) ?? []; - */ - return []; - } - - private handleEndpointsUpdated() { - this.kibanaLink!.label = this.getKibanaLink(); - this.kibanaLink!.url = this.getKibanaLink(); - this.kibanaLoading!.loading = false; - - this.grafanaLink!.label = this.getGrafanaLink(); - this.grafanaLink!.url = this.getGrafanaLink(); - this.grafanaLoading!.loading = false; + const grafanaUrl = this._postgresModel.config.status.metricsDashboard ?? ''; + this.grafanaLink.label = grafanaUrl; + this.grafanaLink.url = grafanaUrl; + this.grafanaLoading.loading = false; + } } private handleRegistrationsUpdated() { @@ -362,19 +286,9 @@ export class PostgresOverviewPage extends DashboardPage { this.propertiesLoading!.loading = false; } - private handleServiceUpdated() { + private handleConfigUpdated() { this.properties!.propertyItems = this.getProperties(); this.propertiesLoading!.loading = false; - - this.nodesTable!.data = this.getNodes(); - this.nodesTableLoading!.loading = false; - } - - private handlePodsUpdated() { - this.properties!.propertyItems = this.getProperties(); - this.propertiesLoading!.loading = false; - - this.nodesTable!.data = this.getNodes(); - this.nodesTableLoading!.loading = false; + this.refreshDashboardLinks(); } } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 393e9b129a..03fb1e7858 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { IconPathHelper, cssStyles } from '../../../constants'; -import { KeyValueContainer, KeyValue } from '../../components/keyValueContainer'; +import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer'; import { DashboardPage } from '../../components/dashboardPage'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; @@ -19,7 +19,7 @@ export class PostgresPropertiesPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); - this.disposables.push(this._postgresModel.onServiceUpdated( + this.disposables.push(this._postgresModel.onConfigUpdated( () => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated()))); this.disposables.push(this._controllerModel.onRegistrationsUpdated( @@ -54,7 +54,7 @@ export class PostgresPropertiesPage extends DashboardPage { this.loading = this.modelView.modelBuilder.loadingComponent() .withItem(this.keyValueContainer.container) .withProperties({ - loading: !this._postgresModel.serviceLastUpdated && !this._controllerModel.registrationsLastUpdated + loading: !this._postgresModel.configLastUpdated && !this._controllerModel.registrationsLastUpdated }).component(); content.addItem(this.loading); @@ -91,24 +91,20 @@ export class PostgresPropertiesPage extends DashboardPage { } private getProperties(): KeyValue[] { - /* - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; - const connectionString = `postgresql://postgres@${endpoint.ip}:${endpoint.port}`; - const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); + const endpoint = this._postgresModel.endpoint; + const status = this._postgresModel.config?.status; return [ - new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, connectionString), + new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''), new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'), - new TextKeyValue(this.modelView.modelBuilder, loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'), + new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown), // TODO: Make this a LinkKeyValue that opens the controller dashboard - new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.namespace ?? ''), - new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.configuration), - new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''), - new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, registration?.resourceGroupName ?? ''), - new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, registration?.subscriptionId ?? '') + new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '') ]; - */ - return []; } private handleRegistrationsUpdated() { diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts deleted file mode 100644 index 2ea339f92b..0000000000 --- a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts +++ /dev/null @@ -1,225 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import * as azdata from 'azdata'; -import * as loc from '../../../localizedConstants'; -import { IconPathHelper, cssStyles } from '../../../constants'; -import { DashboardPage } from '../../components/dashboardPage'; -import { PostgresModel } from '../../../models/postgresModel'; -import { fromNow } from '../../../common/date'; - -export class PostgresResourceHealthPage extends DashboardPage { - private interval: NodeJS.Timeout; - private podsUpdated?: azdata.TextComponent; - private podsLoading?: azdata.LoadingComponent; - private conditionsLoading?: azdata.LoadingComponent; - private podsTable?: azdata.DeclarativeTableComponent; - private conditionsTable?: azdata.DeclarativeTableComponent; - - constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { - super(modelView); - - this.disposables.push( - modelView.onClosed(() => { - try { clearInterval(this.interval); } - catch { } - })); - - this.disposables.push(this._postgresModel.onServiceUpdated( - () => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated()))); - - // Keep the last updated timestamps up to date with the current time - this.interval = setInterval(() => this.handleServiceUpdated(), 60 * 1000); - } - - protected get title(): string { - return loc.resourceHealth; - } - - protected get id(): string { - return 'postgres-resource-health'; - } - - protected get icon(): { dark: string; light: string; } { - return IconPathHelper.health; - } - - protected get container(): azdata.Component { - const root = this.modelView.modelBuilder.divContainer().component(); - const content = this.modelView.modelBuilder.divContainer().component(); - root.addItem(content, { CSSStyles: { 'margin': '20px' } }); - - content.addItem(this.modelView.modelBuilder.text().withProperties({ - value: loc.resourceHealth, - CSSStyles: { ...cssStyles.title, 'margin-bottom': '30px' } - }).component()); - - content.addItem(this.modelView.modelBuilder.text().withProperties({ - value: loc.podOverview, - CSSStyles: { ...cssStyles.title, 'margin-block-end': '0' } - }).component()); - - this.podsUpdated = this.modelView.modelBuilder.text().withProperties({ - value: this.getPodsLastUpdated(), - CSSStyles: { ...cssStyles.text, 'font-size': '12px', 'margin-block-start': '0' } - }).component(); - - content.addItem(this.podsUpdated); - - // Pod overview - this.podsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - columns: [ - { - displayName: '', - valueType: azdata.DeclarativeDataType.string, - width: '35%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { ...cssStyles.tableRow, 'font-size': '20px', 'font-weight': 'bold', 'padding': '7px' } - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.string, - width: '65%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { ...cssStyles.tableRow, 'padding': '7px' } - } - ], - data: this.getPodsTable() - }).component(); - - this.podsLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.podsTable) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated - }).component(); - - content.addItem(this.podsLoading, { CSSStyles: { 'margin-bottom': '30px' } }); - - // Conditions table - this.conditionsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - width: '100%', - columns: [ - { - displayName: loc.condition, - valueType: azdata.DeclarativeDataType.string, - width: '15%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.component, - width: '1%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.details, - valueType: azdata.DeclarativeDataType.string, - width: '64%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.lastUpdated, - valueType: azdata.DeclarativeDataType.string, - width: '20%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { ...cssStyles.tableRow, 'white-space': 'nowrap' } - } - ], - data: this.getConditionsTable() - }).component(); - - this.conditionsLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.conditionsTable) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated - }).component(); - - content.addItem(this.conditionsLoading); - this.initialized = true; - return root; - } - - protected get toolbarContainer(): azdata.ToolbarContainer { - const refreshButton = this.modelView.modelBuilder.button().withProperties({ - label: loc.refresh, - iconPath: IconPathHelper.refresh - }).component(); - - this.disposables.push( - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - this.podsLoading!.loading = true; - this.conditionsLoading!.loading = true; - await this._postgresModel.refresh(); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } finally { - refreshButton.enabled = true; - } - })); - - return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ - { component: refreshButton } - ]).component(); - } - - private getPodsLastUpdated(): string { - return this._postgresModel.serviceLastUpdated - ? loc.updated(fromNow(this._postgresModel.serviceLastUpdated!, true)) : ''; - } - - private getPodsTable(): (string | number)[][] { - return [ - [this._postgresModel.service?.status?.podsRunning ?? 0, loc.running], - [this._postgresModel.service?.status?.podsPending ?? 0, loc.pending], - [this._postgresModel.service?.status?.podsFailed ?? 0, loc.failed], - [this._postgresModel.service?.status?.podsUnknown ?? 0, loc.unknown] - ]; - } - - private getConditionsTable(): (string | azdata.ImageComponent)[][] { - /* TODO chgagnon - return this._postgresModel.service?.status?.conditions?.map(c => { - const healthy = c.type === 'Ready' ? c.status === 'True' : c.status === 'False'; - - const image = this.modelView.modelBuilder.image().withProperties({ - iconPath: healthy ? IconPathHelper.success : IconPathHelper.fail, - iconHeight: '20px', - iconWidth: '20px', - width: '20px', - height: '20px' - }).component(); - - return [ - c.type ?? '', - image, - c.message ?? '', - c.lastTransitionTime ? fromNow(c.lastTransitionTime!, true) : '' - ]; - }) ?? []; - */ - return []; - } - - private handleServiceUpdated() { - this.podsUpdated!.value = this.getPodsLastUpdated(); - this.podsTable!.data = this.getPodsTable(); - this.podsLoading!.loading = false; - - this.conditionsTable!.data = this.getConditionsTable(); - this.conditionsLoading!.loading = false; - } -} diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts index 14a5be718a..34b477daff 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts @@ -3,13 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { IconPathHelper, cssStyles } from '../../../constants'; import { DashboardPage } from '../../components/dashboardPage'; +import { ControllerModel } from '../../../models/controllerModel'; +import { ResourceType } from 'arc'; +import { PostgresModel } from '../../../models/postgresModel'; export class PostgresSupportRequestPage extends DashboardPage { - constructor(protected modelView: azdata.ModelView) { + constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); } @@ -48,15 +52,13 @@ export class PostgresSupportRequestPage extends DashboardPage { this.disposables.push( supportRequestButton.onDidClick(() => { - /* - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); - } else { + const azure = this._controllerModel.controllerConfig?.spec.settings.azure; + if (azure) { vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}/supportrequest`)); + `https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}/supportrequest`)); + } else { + vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration); } - */ })); content.addItem(supportRequestButton); diff --git a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts index 8fad5ee42f..2b65375b13 100644 --- a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts +++ b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ControllerInfo } from 'arc'; +import { ControllerInfo, ResourceInfo } from 'arc'; import * as azdata from 'azdata'; import * as azdataExt from 'azdata-ext'; import { v4 as uuid } from 'uuid'; @@ -74,6 +74,7 @@ abstract class ControllerDialogBase extends InitializingComponent { protected completionPromise = new Deferred(); protected id!: string; + protected resources: ResourceInfo[] = []; constructor(protected treeDataProvider: AzureArcTreeDataProvider, title: string) { super(); @@ -82,6 +83,7 @@ abstract class ControllerDialogBase extends InitializingComponent { public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog { this.id = controllerInfo?.id ?? uuid(); + this.resources = controllerInfo?.resources ?? []; this.dialog.cancelButton.onClick(() => this.handleCancel()); this.dialog.registerContent(async (view) => { this.modelBuilder = view.modelBuilder; @@ -168,7 +170,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase { name: this.nameInputBox.value ?? '', username: this.usernameInputBox.value, rememberPassword: this.rememberPwCheckBox.checked ?? false, - resources: [] + resources: this.resources }; const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value); try { diff --git a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts new file mode 100644 index 0000000000..de9fdd1683 --- /dev/null +++ b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { Deferred } from '../../common/promise'; +import { createCredentialId } from '../../common/utils'; +import { credentialNamespace } from '../../constants'; +import * as loc from '../../localizedConstants'; +import { ControllerModel } from '../../models/controllerModel'; +import { MiaaModel } from '../../models/miaaModel'; +import { InitializingComponent } from '../components/initializingComponent'; + +export class ConnectToSqlDialog extends InitializingComponent { + private modelBuilder!: azdata.ModelBuilder; + + private serverNameInputBox!: azdata.InputBoxComponent; + private usernameInputBox!: azdata.InputBoxComponent; + private passwordInputBox!: azdata.InputBoxComponent; + private rememberPwCheckBox!: azdata.CheckBoxComponent; + + private _completionPromise = new Deferred(); + + constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { + super(); + } + + public showDialog(connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog { + const dialog = azdata.window.createModelViewDialog(loc.connectToSql(this._miaaModel.info.name)); + dialog.cancelButton.onClick(() => this.handleCancel()); + dialog.registerContent(async view => { + this.modelBuilder = view.modelBuilder; + + this.serverNameInputBox = this.modelBuilder.inputBox() + .withProperties({ + value: connectionProfile?.serverName, + enabled: false + }).component(); + this.usernameInputBox = this.modelBuilder.inputBox() + .withProperties({ + value: connectionProfile?.userName + }).component(); + this.passwordInputBox = this.modelBuilder.inputBox() + .withProperties({ + inputType: 'password', + value: connectionProfile?.password + }) + .component(); + this.rememberPwCheckBox = this.modelBuilder.checkBox() + .withProperties({ + label: loc.rememberPassword, + checked: connectionProfile?.savePassword + }).component(); + + let formModel = this.modelBuilder.formContainer() + .withFormItems([{ + components: [ + { + component: this.serverNameInputBox, + title: loc.serverEndpoint, + required: true + }, { + component: this.usernameInputBox, + title: loc.username, + required: true + }, { + component: this.passwordInputBox, + title: loc.password, + required: true + }, { + component: this.rememberPwCheckBox, + title: '' + } + ], + title: '' + }]).withLayout({ width: '100%' }).component(); + await view.initializeModel(formModel); + this.serverNameInputBox.focus(); + this.initialized = true; + }); + + dialog.registerCloseValidator(async () => await this.validate()); + dialog.okButton.label = loc.connect; + dialog.cancelButton.label = loc.cancel; + azdata.window.openDialog(dialog); + return dialog; + } + + public async validate(): Promise { + if (!this.serverNameInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) { + return false; + } + const connectionProfile: azdata.IConnectionProfile = { + serverName: this.serverNameInputBox.value, + databaseName: '', + authenticationType: 'SqlLogin', + providerName: 'MSSQL', + connectionName: '', + userName: this.usernameInputBox.value, + password: this.passwordInputBox.value, + savePassword: !!this.rememberPwCheckBox.checked, + groupFullName: undefined, + saveProfile: true, + id: '', + groupId: undefined, + options: {} + }; + const result = await azdata.connection.connect(connectionProfile, false, false); + if (result.connected) { + connectionProfile.id = result.connectionId; + const credentialProvider = await azdata.credentials.getProvider(credentialNamespace); + if (connectionProfile.savePassword) { + await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name), connectionProfile.password); + } else { + await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name)); + } + this._completionPromise.resolve(connectionProfile); + return true; + } + else { + vscode.window.showErrorMessage(loc.connectToSqlFailed(this.serverNameInputBox.value, result.errorMessage)); + return false; + } + } + + private handleCancel(): void { + this._completionPromise.resolve(undefined); + } + + public waitForClose(): Promise { + return this._completionPromise.promise; + } +} diff --git a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts index cd92471c70..35c7a85a8e 100644 --- a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts +++ b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts @@ -135,10 +135,14 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider[] = []; constructor(public model: ControllerModel, private _context: vscode.ExtensionContext, private _treeDataProvider: AzureArcTreeDataProvider) { super(model.label, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers); @@ -55,7 +57,7 @@ export class ControllerTreeNode extends TreeNode { } } - return this._children; + return this._children.length > 0 ? this._children : [new NoInstancesTreeNode()]; } public async openDashboard(): Promise { @@ -68,14 +70,14 @@ export class ControllerTreeNode extends TreeNode { * @param resourceType The resourceType of the node * @param name The name of the node */ - public getResourceNode(resourceType: string, name: string): ResourceTreeNode | undefined { + public getResourceNode(resourceType: string, name: string): ResourceTreeNode | undefined { return this._children.find(c => c.model?.info.resourceType === resourceType && c.model.info.name === name); } private updateChildren(registrations: Registration[]): void { - const newChildren: ResourceTreeNode[] = []; + const newChildren: ResourceTreeNode[] = []; registrations.forEach(registration => { if (!registration.instanceName) { console.warn('Registration is missing required name value, skipping'); @@ -83,7 +85,7 @@ export class ControllerTreeNode extends TreeNode { } const resourceInfo: ResourceInfo = { - name: parseInstanceName(registration.instanceName), + name: registration.instanceName, resourceType: registration.instanceType ?? '' }; @@ -100,10 +102,14 @@ export class ControllerTreeNode extends TreeNode { switch (registration.instanceType) { case ResourceType.postgresInstances: - const postgresModel = new PostgresModel(resourceInfo, registration); + const postgresModel = new PostgresModel(this.model, resourceInfo, registration); node = new PostgresTreeNode(postgresModel, this.model, this._context); break; case ResourceType.sqlManagedInstances: + // Fill in the username too if we already have it + (resourceInfo as MiaaResourceInfo).userName = (this.model.info.resources.find(info => + info.name === resourceInfo.name && + info.resourceType === resourceInfo.resourceType) as MiaaResourceInfo)?.userName; const miaaModel = new MiaaModel(this.model, resourceInfo, registration, this._treeDataProvider); node = new MiaaTreeNode(miaaModel, this.model); break; diff --git a/extensions/arc/src/ui/tree/miaaTreeNode.ts b/extensions/arc/src/ui/tree/miaaTreeNode.ts index 4e8556874e..cd6509b46c 100644 --- a/extensions/arc/src/ui/tree/miaaTreeNode.ts +++ b/extensions/arc/src/ui/tree/miaaTreeNode.ts @@ -8,15 +8,15 @@ import * as vscode from 'vscode'; import { ControllerModel } from '../../models/controllerModel'; import { MiaaModel } from '../../models/miaaModel'; import { MiaaDashboard } from '../dashboards/miaa/miaaDashboard'; -import { TreeNode } from './treeNode'; +import { ResourceTreeNode } from './resourceTreeNode'; /** * The TreeNode for displaying a SQL Managed Instance on Azure Arc */ -export class MiaaTreeNode extends TreeNode { +export class MiaaTreeNode extends ResourceTreeNode { - constructor(public model: MiaaModel, private _controllerModel: ControllerModel) { - super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances); + constructor(model: MiaaModel, private _controllerModel: ControllerModel) { + super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances, model); } public async openDashboard(): Promise { diff --git a/extensions/arc/src/ui/tree/noInstancesTreeNode.ts b/extensions/arc/src/ui/tree/noInstancesTreeNode.ts new file mode 100644 index 0000000000..0af884108b --- /dev/null +++ b/extensions/arc/src/ui/tree/noInstancesTreeNode.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as loc from '../../localizedConstants'; +import { TreeNode } from './treeNode'; + +/** + * A placeholder TreeNode to display when there aren't any child instances available + */ +export class NoInstancesTreeNode extends TreeNode { + + constructor() { + super(loc.noInstancesAvailable, vscode.TreeItemCollapsibleState.None, ''); + } +} diff --git a/extensions/arc/src/ui/tree/postgresTreeNode.ts b/extensions/arc/src/ui/tree/postgresTreeNode.ts index 42c8885c06..2d3dcb5633 100644 --- a/extensions/arc/src/ui/tree/postgresTreeNode.ts +++ b/extensions/arc/src/ui/tree/postgresTreeNode.ts @@ -13,14 +13,14 @@ import { ResourceTreeNode } from './resourceTreeNode'; /** * The TreeNode for displaying an Postgres Server group */ -export class PostgresTreeNode extends ResourceTreeNode { +export class PostgresTreeNode extends ResourceTreeNode { - constructor(private _model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) { - super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model); + constructor(model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) { + super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, model); } public async openDashboard(): Promise { - const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model); + const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this.model); await postgresDashboard.showDashboard(); } } diff --git a/extensions/arc/src/ui/tree/refreshTreeNode.ts b/extensions/arc/src/ui/tree/refreshTreeNode.ts index 7fee0d54f1..7bffa9a512 100644 --- a/extensions/arc/src/ui/tree/refreshTreeNode.ts +++ b/extensions/arc/src/ui/tree/refreshTreeNode.ts @@ -14,7 +14,7 @@ import { refreshActionId } from '../../constants'; export class RefreshTreeNode extends TreeNode { constructor(private _parent: TreeNode) { - super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, 'refresh'); + super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, ''); } public command: vscode.Command = { diff --git a/extensions/arc/src/ui/tree/resourceTreeNode.ts b/extensions/arc/src/ui/tree/resourceTreeNode.ts index 834dbd6467..b916bafe69 100644 --- a/extensions/arc/src/ui/tree/resourceTreeNode.ts +++ b/extensions/arc/src/ui/tree/resourceTreeNode.ts @@ -10,8 +10,8 @@ import { TreeNode } from './treeNode'; /** * A TreeNode belonging to a child of a Controller */ -export abstract class ResourceTreeNode extends TreeNode { - constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType?: string, public model?: ResourceModel) { +export abstract class ResourceTreeNode extends TreeNode { + constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType: string, public model: M) { super(label, collapsibleState, resourceType); } } diff --git a/extensions/azdata/README.md b/extensions/azdata/README.md index cdb5ee3e29..fadee9b62c 100644 --- a/extensions/azdata/README.md +++ b/extensions/azdata/README.md @@ -2,7 +2,7 @@ Welcome to Microsoft Azure Data CLI Extension for Azure Data Studio! -**This extension is only applicable to customers in the Azure Arc data services private preview. Other usage is not supported at this time.** +**This extension is only applicable to customers in the Azure Arc data services public preview. Other usage is not supported at this time.** ## Overview diff --git a/extensions/azdata/images/extension.png b/extensions/azdata/images/extension.png index c86d6d1e00..7a041a392d 100644 Binary files a/extensions/azdata/images/extension.png and b/extensions/azdata/images/extension.png differ diff --git a/extensions/azdata/package.json b/extensions/azdata/package.json index 5ef3b7ece0..e046391cde 100644 --- a/extensions/azdata/package.json +++ b/extensions/azdata/package.json @@ -2,14 +2,14 @@ "name": "azdata", "displayName": "%azdata.displayName%", "description": "%azdata.description%", - "version": "0.1.2", + "version": "0.3.1", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "icon": "images/extension.png", "engines": { "vscode": "*", - "azdata": ">=1.20.0" + "azdata": ">=1.22.0" }, "activationEvents": [ "*" @@ -132,5 +132,10 @@ "sinon": "^9.0.2", "typemoq": "^2.1.0", "vscodetestcover": "^1.1.0" + }, + "__metadata": { + "id": "73", + "publisherDisplayName": "Microsoft", + "publisherId": "Microsoft" } } diff --git a/extensions/azdata/src/api.ts b/extensions/azdata/src/api.ts index ec32f0978b..70b3d0fe4f 100644 --- a/extensions/azdata/src/api.ts +++ b/extensions/azdata/src/api.ts @@ -72,6 +72,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.postgres.server.delete(name); + }, list: async () => { await localAzdataDiscovered; throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); @@ -81,6 +86,26 @@ export function getAzdataApi(localAzdataDiscovered: Promise { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars); } } }, diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index 28d859f753..4ea09ffae7 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -6,13 +6,15 @@ import * as azdataExt from 'azdata-ext'; import * as fs from 'fs'; import * as os from 'os'; +import * as path from 'path'; import { SemVer } from 'semver'; import * as vscode from 'vscode'; +import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo'; import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess'; import { HttpClient } from './common/httpClient'; import Logger from './common/logger'; -import { getErrorMessage, searchForCmd } from './common/utils'; -import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataHostname, azdataInstallKey, azdataReleaseJson, azdataUpdateKey, azdataUri, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants'; +import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils'; +import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants'; import * as loc from './localizedConstants'; const enum AzdataDeployOption { @@ -92,11 +94,44 @@ export class AzdataTool implements azdataExt.IAzdataApi { }, postgres: { server: { + delete: async (name: string) => { + return this.executeCommand(['arc', 'postgres', 'server', 'delete', '-n', name]); + }, list: async () => { return this.executeCommand(['arc', 'postgres', 'server', 'list']); }, show: async (name: string) => { return this.executeCommand(['arc', 'postgres', 'server', 'show', '-n', name]); + }, + edit: async ( + name: string, + args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + additionalEnvVars?: { [key: string]: string }) => { + const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name]; + if (args.adminPassword) { argsArray.push('--admin-password'); } + if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); } + if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); } + if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); } + if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); } + if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); } + if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); } + if (args.noWait) { argsArray.push('--no-wait'); } + if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); } + if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); } + if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); } + return this.executeCommand(argsArray, additionalEnvVars); } } }, @@ -151,18 +186,18 @@ export class AzdataTool implements azdataExt.IAzdataApi { // ERROR: { stderr: '...' } // so we also need to trim off the start that isn't a valid JSON blob err.stderr = JSON.parse(err.stderr.substring(err.stderr.indexOf('{'), err.stderr.indexOf('}') + 1)).stderr; - } catch (err) { + } catch { // it means this was probably some other generic error (such as command not being found) // check if azdata still exists if it does then rethrow the original error if not then emit a new specific error. try { await fs.promises.access(this._path); //this.path exists - throw err; // rethrow the error } catch (e) { // this.path does not exist await vscode.commands.executeCommand('setContext', azdataFound, false); - throw (loc.noAzdata); + throw new NoAzdataError(); } + throw err; // rethrow the original error } } @@ -311,6 +346,7 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise { + const downLoadLink = await getPlatformDownloadLink(); const downloadFolder = os.tmpdir(); - const downloadedFile = await HttpClient.downloadFile(`${azdataHostname}/${azdataUri}`, downloadFolder); - await executeCommand('msiexec', ['/qn', '/i', downloadedFile]); + const downloadLogs = path.join(downloadFolder, 'ads_azdata_install_logs.log'); + const downloadedFile = await HttpClient.downloadFile(downLoadLink, downloadFolder); + + try { + await executeSudoCommand(`msiexec /qn /i "${downloadedFile}" /lvx "${downloadLogs}"`); + } catch (err) { + throw new Error(`${err.message}. See logs at ${downloadLogs} for more details.`); + } } /** @@ -502,27 +546,10 @@ export async function discoverLatestAvailableAzdataVersion(): Promise { // However, doing discovery that way required apt update to be performed which requires sudo privileges. At least currently this code path // gets invoked on extension start up and prompt user for sudo privileges is annoying at best. So for now basing linux discovery also on a releaseJson file. default: - return await discoverLatestAzdataVersionFromJson(); + return await getPlatformReleaseVersion(); } } -/** - * Gets the latest azdata version from a json document published by azdata release - */ -async function discoverLatestAzdataVersionFromJson(): Promise { - // get version information for current platform from http://aka.ms/azdata/release.json - const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`); - let azdataReleaseInfo; - try { - azdataReleaseInfo = JSON.parse(fileContents); - } catch (e) { - throw Error(`failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}, text being parsed: '${fileContents}', error:${getErrorMessage(e)}`); - } - const version = azdataReleaseInfo[process.platform]['version']; - Logger.log(loc.latestAzdataVersionAvailable(version)); - return new SemVer(version); -} - /** * Parses out the azdata version from the raw azdata version output * @param raw The raw version output from azdata --version diff --git a/extensions/azdata/src/azdataReleaseInfo.ts b/extensions/azdata/src/azdataReleaseInfo.ts new file mode 100644 index 0000000000..adac99ad7d --- /dev/null +++ b/extensions/azdata/src/azdataReleaseInfo.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as loc from './localizedConstants'; +import { SemVer } from 'semver'; +import { HttpClient } from './common/httpClient'; +import Logger from './common/logger'; +import { getErrorMessage } from './common/utils'; +import { azdataHostname, azdataReleaseJson } from './constants'; + +interface PlatformReleaseInfo { + version: string; // "20.0.1" + link?: string; // "https://aka.ms/azdata-msi" +} + +interface AzdataReleaseInfo { + win32: PlatformReleaseInfo, + darwin: PlatformReleaseInfo, + linux: PlatformReleaseInfo +} + +function getPlatformAzdataReleaseInfo(releaseInfo: AzdataReleaseInfo): PlatformReleaseInfo { + switch (os.platform()) { + case 'win32': + return releaseInfo.win32; + case 'linux': + return releaseInfo.linux; + case 'darwin': + return releaseInfo.darwin; + default: + Logger.log(loc.platformUnsupported(os.platform())); + throw new Error(`Unsupported AzdataReleaseInfo platform '${os.platform()}`); + } +} + +/** + * Gets the release version for the current platform from the release info - throwing an error if it doesn't exist. + * @param releaseInfo The AzdataReleaseInfo object + */ +export async function getPlatformReleaseVersion(): Promise { + const releaseInfo = await getAzdataReleaseInfo(); + const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo); + if (!platformReleaseInfo.version) { + Logger.log(loc.noReleaseVersion(os.platform(), JSON.stringify(releaseInfo))); + throw new Error(`No release version available for platform ${os.platform()}`); + } + Logger.log(loc.latestAzdataVersionAvailable(platformReleaseInfo.version)); + return new SemVer(platformReleaseInfo.version); +} + +/** + * Gets the download link for the current platform from the release info - throwing an error if it doesn't exist. + * @param releaseInfo The AzdataReleaseInfo object + */ +export async function getPlatformDownloadLink(): Promise { + const releaseInfo = await getAzdataReleaseInfo(); + const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo); + if (!platformReleaseInfo.link) { + Logger.log(loc.noDownloadLink(os.platform(), JSON.stringify(releaseInfo))); + throw new Error(`No download link available for platform ${os.platform()}`); + } + return platformReleaseInfo.link; +} + +async function getAzdataReleaseInfo(): Promise { + const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`); + try { + return JSON.parse(fileContents); + } catch (e) { + Logger.log(loc.failedToParseReleaseInfo(`${azdataHostname}/${azdataReleaseJson}`, fileContents, e)); + throw Error(`Failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}. Error: ${getErrorMessage(e)}`); + } +} diff --git a/extensions/azdata/src/common/childProcess.ts b/extensions/azdata/src/common/childProcess.ts index b4620c2748..4e6d9a4a21 100644 --- a/extensions/azdata/src/common/childProcess.ts +++ b/extensions/azdata/src/common/childProcess.ts @@ -65,7 +65,7 @@ export async function executeCommand(command: string, args: string[], additional Logger.log(loc.stdoutOutput(stdout)); } if (stderr) { - Logger.log(loc.stdoutOutput(stderr)); + Logger.log(loc.stderrOutput(stderr)); } if (code) { const err = new ExitCodeError(code, stderr); @@ -94,7 +94,7 @@ export async function executeSudoCommand(command: string): Promise { + Logger.log(loc.downloadFinished); + resolve(strings.join('')); + }); } let contentLength = response.headers['content-length']; let totalBytes = parseInt(contentLength || '0'); @@ -92,13 +97,6 @@ export namespace HttpClient { printThreshold += 0.1; } } - }) - .on('close', async () => { - if (targetFolder === undefined) { - - Logger.log(loc.downloadFinished); - resolve(strings.join('')); - } }); }); } diff --git a/extensions/azdata/src/common/logger.ts b/extensions/azdata/src/common/logger.ts index c4f716d85e..a80dfca2a7 100644 --- a/extensions/azdata/src/common/logger.ts +++ b/extensions/azdata/src/common/logger.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as loc from '../localizedConstants'; export class Log { private _output: vscode.OutputChannel; constructor() { - this._output = vscode.window.createOutputChannel('azdata'); + this._output = vscode.window.createOutputChannel(loc.azdata); } log(msg: string): void { - this._output.appendLine(msg); + this._output.appendLine(`[${new Date().toISOString()}] ${msg}`); } show(): void { diff --git a/extensions/azdata/src/common/utils.ts b/extensions/azdata/src/common/utils.ts index 023406586c..20106c28bd 100644 --- a/extensions/azdata/src/common/utils.ts +++ b/extensions/azdata/src/common/utils.ts @@ -7,7 +7,6 @@ import * as azdataExt from 'azdata-ext'; import * as which from 'which'; import * as loc from '../localizedConstants'; - export class NoAzdataError extends Error implements azdataExt.ErrorWithLink { constructor() { super(loc.noAzdata); @@ -17,7 +16,6 @@ export class NoAzdataError extends Error implements azdataExt.ErrorWithLink { return loc.noAzdataWithLink; } } - /** * Searches for the first instance of the specified executable in the PATH environment variable * @param exe The executable to search for diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts index 0cdb1389b4..8697e3e334 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -34,15 +34,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { - eulaAccepted = userResponse; - }) - .catch((err) => console.log(err)); - } // Don't block on this since we want the extension to finish activating without needing user input const localAzdataDiscovered = checkAndInstallAzdata() // install if not installed and user wants it. diff --git a/extensions/azdata/src/localizedConstants.ts b/extensions/azdata/src/localizedConstants.ts index 6f0037cc3c..351ac798e0 100644 --- a/extensions/azdata/src/localizedConstants.ts +++ b/extensions/azdata/src/localizedConstants.ts @@ -4,16 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; +import { getErrorMessage } from './common/utils'; import { azdataConfigSection, azdataInstallKey, azdataUpdateKey } from './constants'; const localize = nls.loadMessageBundle(); +export const azdata = localize('azdata.azdata', "Azure Data CLI"); export const searchingForAzdata = localize('azdata.searchingForAzdata', "Searching for existing Azure Data CLI installation..."); export const foundExistingAzdata = (path: string, version: string): string => localize('azdata.foundExistingAzdata', "Found existing Azure Data CLI installation of version (v{0}) at path:{1}", version, path); export const downloadingProgressMb = (currentMb: string, totalMb: string): string => localize('azdata.downloadingProgressMb', "Downloading ({0} / {1} MB)", currentMb, totalMb); export const downloadFinished = localize('azdata.downloadFinished', "Download finished"); -export const installingAzdata = localize('azdata.installingAzdata', "Installing azdata..."); -export const updatingAzdata = localize('azdata.updatingAzdata', "updating azdata..."); +export const installingAzdata = localize('azdata.installingAzdata', "Installing Azure Data CLI..."); +export const updatingAzdata = localize('azdata.updatingAzdata', "Updating Azure Data CLI..."); export const azdataInstalled = localize('azdata.azdataInstalled', "Azure Data CLI was successfully installed. Restarting Azure Data Studio is required to complete configuration - features will not be activated until this is done."); export const azdataUpdated = (version: string) => localize('azdata.azdataUpdated', "Azure Data CLI was successfully updated to version: {0}.", version); export const yes = localize('azdata.yes', "Yes"); @@ -22,46 +24,45 @@ export const accept = localize('azdata.accept', "Accept"); export const decline = localize('azdata.decline', "Decline"); export const doNotAskAgain = localize('azdata.doNotAskAgain', "Don't Ask Again"); export const askLater = localize('azdata.askLater', "Ask Later"); -export const downloadingTo = (name: string, location: string): string => localize('azdata.downloadingTo', "Downloading {0} to {1}", name, location); +export const downloadingTo = (name: string, url: string, location: string): string => localize('azdata.downloadingTo', "Downloading {0} from {1} to {2}", name, url, location); export const executingCommand = (command: string, args: string[]): string => localize('azdata.executingCommand', "Executing command: '{0} {1}'", command, args?.join(' ')); export const stdoutOutput = (stdout: string): string => localize('azdata.stdoutOutput', "stdout: {0}", stdout); export const stderrOutput = (stderr: string): string => localize('azdata.stderrOutput', "stderr: {0}", stderr); -export const checkingLatestAzdataVersion = localize('azdata.checkingLatestAzdataVersion', "Checking for latest available version of azdata"); +export const checkingLatestAzdataVersion = localize('azdata.checkingLatestAzdataVersion', "Checking for latest available version of Azure Data CLI"); export const gettingTextContentsOfUrl = (url: string): string => localize('azdata.gettingTextContentsOfUrl', "Getting text contents of resource at URL {0}", url); export const foundAzdataVersionToUpdateTo = (newVersion: string, currentVersion: string): string => localize('azdata.versionForUpdate', "Found version: {0} that Azure Data CLI can be updated to from current version: {1}.", newVersion, currentVersion); export const latestAzdataVersionAvailable = (version: string): string => localize('azdata.latestAzdataVersionAvailable', "Latest available Azure Data CLI version: {0}.", version); -export const couldNotFindAzdata = (err: any): string => localize('azdata.couldNotFindAzdata', "Could not find azdata. Error: {0}", err.message ?? err); -export const currentlyInstalledVersionIsLatest = (currentVersion: string): string => localize('azdata.currentlyInstalledVersionIsLatest', "Currently installed version of azdata: {0} is same or newer than any other version available", currentVersion); +export const couldNotFindAzdata = (err: any): string => localize('azdata.couldNotFindAzdata', "Could not find Azure Data CLI. Error: {0}", err.message ?? err); +export const currentlyInstalledVersionIsLatest = (currentVersion: string): string => localize('azdata.currentlyInstalledVersionIsLatest', "Currently installed version of Azure Data CLI: {0} is same or newer than any other version available", currentVersion); export const promptLog = (logEntry: string) => localize('azdata.promptLog', "Prompting the user to accept the following: {0}", logEntry); -export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find azdata, install it now? If not then some features will not be able to function."); +export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function."); export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall); export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version); export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version)); export const downloadError = localize('azdata.downloadError', "Error while downloading"); -export const installError = (err: any): string => localize('azdata.installError', "Error installing azdata: {0}", err.message ?? err); -export const updateError = (err: any): string => localize('azdata.updateError', "Error updating azdata: {0}", err.message ?? err); +export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err); +export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err); export const platformUnsupported = (platform: string): string => localize('azdata.platformUnsupported', "Platform '{0}' is currently unsupported", platform); export const unexpectedCommandError = (errMsg: string): string => localize('azdata.unexpectedCommandError', "Unexpected error executing command: {0}", errMsg); export const unexpectedExitCode = (code: number, err: string): string => localize('azdata.unexpectedExitCode', "Unexpected exit code from command: {1} ({0})", code, err); -export const noAzdata = localize('azdata.NoAzdata', "No Azure Data CLI is available, [install the Azure Data CLI](command:azdata.install) to enable the features that require it."); +export const noAzdata = localize('azdata.noAzdata', "No Azure Data CLI is available, run the command 'Azure Data CLI: Install' to enable the features that require it."); export const noAzdataWithLink = localize('azdata.noAzdataWithLink', "No Azure Data CLI is available, [install the Azure Data CLI](command:azdata.install) to enable the features that require it."); -export const skipInstall = (config: string): string => localize('azdata.skipInstall', "Skipping installation of azdata, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataInstallKey, config); -export const skipUpdate = (config: string): string => localize('azdata.skipUpdate', "Skipping update of azdata, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataUpdateKey, config); - +export const skipInstall = (config: string): string => localize('azdata.skipInstall', "Skipping installation of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataInstallKey, config); +export const skipUpdate = (config: string): string => localize('azdata.skipUpdate', "Skipping update of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataUpdateKey, config); +export const noReleaseVersion = (platform: string, releaseInfo: string): string => localize('azdata.noReleaseVersion', "No release version available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo); +export const noDownloadLink = (platform: string, releaseInfo: string): string => localize('azdata.noDownloadLink', "No download link available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo); +export const failedToParseReleaseInfo = (url: string, fileContents: string, err: any): string => localize('azdata.failedToParseReleaseInfo', "Failed to parse the JSON of contents at: {0}.\nFile contents:\n{1}\nError: {2}", url, fileContents, getErrorMessage(err)); export const azdataUserSettingRead = (configName: string, configValue: string): string => localize('azdata.azdataUserSettingReadLog', "Azure Data CLI user setting: {0}.{1} read, value: {2}", azdataConfigSection, configName, configValue); export const azdataUserSettingUpdated = (configName: string, configValue: string): string => localize('azdata.azdataUserSettingUpdatedLog', "Azure Data CLI user setting: {0}.{1} updated, newValue: {2}", azdataConfigSection, configName, configValue); -export const userResponseToInstallPrompt = (response: string | undefined): string => localize('azdata.userResponseInstall', "User Response on prompt to install azdata: {0}", response); -export const userResponseToUpdatePrompt = (response: string | undefined): string => localize('azdata.userResponseUpdate', "User Response on prompt to update azdata: {0}", response); +export const userResponseToInstallPrompt = (response: string | undefined): string => localize('azdata.userResponseInstall', "User Response on prompt to install Azure Data CLI: {0}", response); +export const userResponseToUpdatePrompt = (response: string | undefined): string => localize('azdata.userResponseUpdate', "User Response on prompt to update Azure Data CLI: {0}", response); export const userRequestedInstall = localize('azdata.userRequestedInstall', "User requested to install Azure Data CLI using 'Azure Data CLI: Install' command"); export const userRequestedUpdate = localize('azdata.userRequestedUpdate', "User requested to update Azure Data CLI using 'Azure Data CLI: Check for Update' command"); export const userRequestedAcceptEula = localize('azdata.acceptEula', "User requested to be prompted for accepting EULA by invoking 'Azure Data CLI: Accept EULA' command"); export const updateCheckSkipped = localize('azdata.updateCheckSkipped', "No check for new Azure Data CLI version availability performed as Azure Data CLI was not found to be installed"); export const eulaNotAccepted = localize('azdata.eulaNotAccepted', "Microsoft Privacy statement and Azure Data CLI license terms have not been accepted. Execute the command: [Azure Data CLI: Accept EULA](command:azdata.acceptEula) to accept EULA to enable the features that requires Azure Data CLI."); -export const installManually = (expectedVersion: string, instructionsUrl: string) => localize('azdata.installManually', "Azure Data CLI is not installed. Version: {0} needs to be installed or some features may not work. Please install it manually using these [instructions]({1}). Restart ADS when installation is done.", expectedVersion, instructionsUrl); -export const installCorrectVersionManually = (currentVersion: string, expectedVersion: string, instructionsUrl: string) => localize('azdata.installCorrectVersionManually', "Azure Data CLI version: {0} is installed, version: {1} needs to be installed or some features may not work. Please uninstall the current version and then install the correct version manually using these [instructions]({2}). Restart ADS when installation is done.", currentVersion, expectedVersion, instructionsUrl); export const promptForEula = (privacyStatementUrl: string, eulaUrl: string) => localize('azdata.promptForEula', "It is required to accept the [Microsoft Privacy Statement]({0}) and the [Azure Data CLI license terms]({1}) to use this extension. Declining this will result in some features not working.", privacyStatementUrl, eulaUrl); export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl)); export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response); export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted); -export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'EULA Accepted' state to: {0}", eulaAccepted); diff --git a/extensions/azdata/src/test/azdata.test.ts b/extensions/azdata/src/test/azdata.test.ts index d15414eec7..e1725c82b1 100644 --- a/extensions/azdata/src/test/azdata.test.ts +++ b/extensions/azdata/src/test/azdata.test.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; import * as should from 'should'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; @@ -11,10 +10,14 @@ import * as azdata from '../azdata'; import * as childProcess from '../common/childProcess'; import { HttpClient } from '../common/httpClient'; import * as utils from '../common/utils'; -import * as constants from '../constants'; import * as loc from '../localizedConstants'; const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0'); + +/** + * This matches the schema of the JSON file used to determine the current version of + * azdata - do not modify unless also updating the corresponding JSON file + */ const releaseJson = { win32: { 'version': '9999.999.999', @@ -27,7 +30,7 @@ const releaseJson = { 'version': '9999.999.999' } }; - +let executeSudoCommandStub: sinon.SinonStub; describe('azdata', function () { afterEach(function (): void { @@ -55,9 +58,11 @@ describe('azdata', function () { }); describe('installAzdata', function (): void { + beforeEach(function (): void { sinon.stub(vscode.window, 'showErrorMessage').returns(Promise.resolve(loc.yes)); sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata')); + executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' })); }); it('successful install', async function (): Promise { @@ -77,8 +82,12 @@ describe('azdata', function () { if (process.platform === 'win32') { it('unsuccessful download - win32', async function (): Promise { sinon.stub(HttpClient, 'downloadFile').rejects(); - const downloadPromise = azdata.checkAndInstallAzdata(); - await should(downloadPromise).be.rejected(); + sinon.stub(childProcess, 'executeCommand') + .onFirstCall() + .rejects(new Error('not Found')) // First call mock the tool not being found + .resolves({ stdout: '1.0.0', stderr: '' }); + const azdataTool = await azdata.checkAndInstallAzdata(); + should(azdataTool).be.undefined(); }); } @@ -100,6 +109,7 @@ describe('azdata', function () { describe('updateAzdata', function (): void { beforeEach(function (): void { sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(loc.yes)); + executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' })); }); it('successful update', async function (): Promise { @@ -107,7 +117,6 @@ describe('azdata', function () { case 'win32': await testWin32SuccessfulUpdate(); break; - case 'darwin': await testDarwinSuccessfulUpdate(); break; @@ -132,9 +141,8 @@ describe('azdata', function () { }); describe('discoverLatestAvailableAzdataVersion', function (): void { - this.timeout(20000); - it(`finds latest available version of azdata successfully`, async function (): Promise { - // if the latest version is not discovered then the following call throws failing the test + it('finds latest available version of azdata successfully', async function (): Promise { + sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson)); await azdata.discoverLatestAvailableAzdataVersion(); }); }); @@ -142,7 +150,7 @@ describe('azdata', function () { }); async function testLinuxUnsuccessfulUpdate() { - const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects(); + executeSudoCommandStub.rejects(); const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock); should(updateDone).be.false(); should(executeSudoCommandStub.calledOnce).be.true(); @@ -172,7 +180,7 @@ async function testDarwinUnsuccessfulUpdate() { return Promise.reject(new Error('not Found')); }) .callsFake(async (_command: string, _args: string[]) => { // by default return success - return Promise.resolve({stderr: '', stdout: 'success'}); + return Promise.resolve({ stderr: '', stdout: 'success' }); }); const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock); should(updateDone).be.false(); @@ -181,16 +189,16 @@ async function testDarwinUnsuccessfulUpdate() { async function testWin32UnsuccessfulUpdate() { sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename)); - const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects(); + executeSudoCommandStub.rejects(); const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock); - should(updateDone).be.false(); - should(executeCommandStub.calledOnce).be.true(); + should(updateDone).be.false('Update should not have been successful'); + should(executeSudoCommandStub.calledOnce).be.true(); } async function testLinuxSuccessfulUpdate() { sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson))); const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' })); - const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' })); + executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' }); await azdata.checkAndUpdateAzdata(oldAzdataMock); should(executeSudoCommandStub.callCount).be.equal(6); should(executeCommandStub.calledOnce).be.true(); @@ -209,51 +217,39 @@ async function testDarwinSuccessfulUpdate() { }]; const executeCommandStub = sinon.stub(childProcess, 'executeCommand') .onThirdCall() //third call is brew info azdata-cli --json which needs to return json of new available azdata versions. - .callsFake(async (command: string, args: string[]) => { - should(command).be.equal('brew'); - should(args).deepEqual(['info', 'azdata-cli', '--json']); - return Promise.resolve({ - stderr: '', - stdout: JSON.stringify(brewInfoOutput) - }); + .resolves({ + stderr: '', + stdout: JSON.stringify(brewInfoOutput) }) - .callsFake(async (_command: string, _args: string[]) => { // return success on all other command executions - return Promise.resolve({ stdout: '0.0.0', stderr: '' }); - }); + .resolves({ stdout: '0.0.0', stderr: '' }); await azdata.checkAndUpdateAzdata(oldAzdataMock); should(executeCommandStub.callCount).be.equal(6); + should(executeCommandStub.getCall(2).args[0]).be.equal('brew', '3rd call should have been to brew'); + should(executeCommandStub.getCall(2).args[1]).deepEqual(['info', 'azdata-cli', '--json'], '3rd call did not have expected arguments'); } + async function testWin32SuccessfulUpdate() { sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson))); sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename)); - const executeCommandStub = sinon.stub(childProcess, 'executeCommand').callsFake(async (command: string, args: string[]) => { - should(command).be.equal('msiexec'); - should(args[0]).be.equal('/qn'); - should(args[1]).be.equal('/i'); - should(path.basename(args[2])).be.equal(constants.azdataUri); - return { stdout: '0.0.0', stderr: '' }; - }); await azdata.checkAndUpdateAzdata(oldAzdataMock); - should(executeCommandStub.calledOnce).be.true(); + should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once'); + should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i'); } async function testWin32SuccessfulInstall() { + sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson))); sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename)); const executeCommandStub = sinon.stub(childProcess, 'executeCommand') .onFirstCall() - .callsFake(async (_command: string, _args: string[]) => { - return Promise.reject(new Error('not Found')); - }) - .callsFake(async (command: string, args: string[]) => { - should(command).be.equal('msiexec'); - should(args[0]).be.equal('/qn'); - should(args[1]).be.equal('/i'); - should(path.basename(args[2])).be.equal(constants.azdataUri); - return { stdout: '0.0.0', stderr: '' }; - }); + .rejects(new Error('not Found')) // First call mock the tool not being found + .resolves({ stdout: '1.0.0', stderr: '' }); + executeSudoCommandStub + .returns({ stdout: '', stderr: '' }); await azdata.checkAndInstallAzdata(); - should(executeCommandStub.calledTwice).be.true(); + should(executeCommandStub.calledTwice).be.true(`executeCommand should have been called twice. Actual ${executeCommandStub.getCalls().length}`); + should(executeSudoCommandStub.calledOnce).be.true(`executeSudoCommand should have been called once. Actual ${executeSudoCommandStub.getCalls().length}`); + should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i'); } async function testDarwinSuccessfulInstall() { @@ -272,23 +268,17 @@ async function testDarwinSuccessfulInstall() { async function testLinuxSuccessfulInstall() { const executeCommandStub = sinon.stub(childProcess, 'executeCommand') .onFirstCall() - .callsFake(async (_command: string, _args: string[]) => { - return Promise.reject(new Error('not Found')); - }) - .callsFake(async (_command: string, _args: string[]) => { - return Promise.resolve({ stdout: '0.0.0', stderr: '' }); - }); - const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand') - .callsFake(async (_command: string ) => { - return Promise.resolve({ stdout: 'success', stderr: '' }); - }); + .rejects(new Error('not Found')) + .resolves({ stdout: '0.0.0', stderr: '' }); + executeSudoCommandStub + .resolves({ stdout: 'success', stderr: '' }); await azdata.checkAndInstallAzdata(); should(executeSudoCommandStub.callCount).be.equal(6); should(executeCommandStub.calledThrice).be.true(); } async function testLinuxUnsuccessfulInstall() { - const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects(); + executeSudoCommandStub.rejects(); const downloadPromise = azdata.installAzdata(); await should(downloadPromise).be.rejected(); should(executeSudoCommandStub.calledOnce).be.true(); @@ -302,9 +292,9 @@ async function testDarwinUnsuccessfulInstall() { } async function testWin32UnsuccessfulInstall() { - const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects(); + executeSudoCommandStub.rejects(); sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename)); const downloadPromise = azdata.installAzdata(); await should(downloadPromise).be.rejected(); - should(executeCommandStub.calledOnce).be.true(); + should(executeSudoCommandStub.calledOnce).be.true(); } diff --git a/extensions/azdata/src/test/common/httpClient.test.ts b/extensions/azdata/src/test/common/httpClient.test.ts index 3d79597201..3ddb2c45bd 100644 --- a/extensions/azdata/src/test/common/httpClient.test.ts +++ b/extensions/azdata/src/test/common/httpClient.test.ts @@ -73,12 +73,12 @@ describe('HttpClient', function (): void { }); describe('getTextContent', function (): void { - it.skip('Gets file contents correctly', async function (): Promise { + it('Gets file contents correctly', async function (): Promise { nock('https://127.0.0.1') .get('/arbitraryFile') .replyWithFile(200, __filename); const receivedContents = await HttpClient.getTextContent(`https://127.0.0.1/arbitraryFile`); - should(receivedContents).equal(await fs.promises.readFile(__filename)); + should(receivedContents).equal((await fs.promises.readFile(__filename)).toString()); }); it('rejects on response error', async function (): Promise { diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts index e79ca9e2c7..8eef9ced13 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -19,7 +19,7 @@ declare module 'azdata-ext' { export interface ErrorWithLink extends Error { messageWithLink: string; } - + export interface DcEndpointListResult { description: string, // "Management Proxy" endpoint: string, // "https://10.91.86.39:30777" @@ -143,25 +143,13 @@ declare module 'azdata-ext' { }, status: { readyReplicas: string, // "1/1" - state: string, // "Ready" + state: string, // "Ready", + logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:miaa1')) + metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=miaa1-0 externalEndpoint?: string // "10.91.86.39:32718" } } - export interface PostgresServerShowResult { - apiVersion: string, // "arcdata.microsoft.com/v1alpha1" - kind: string, // "postgresql-12" - metadata: { - creationTimestamp: string, // "2020-08-19T20:25:11Z" - generation: number, // 1 - name: string, // "chgagnon-pg" - namespace: string, // "arc", - resourceVersion: string, // "214944", - selfLink: string, // "/apis/arcdata.microsoft.com/v1alpha1/namespaces/arc/postgresql-12s/chgagnon-pg", - uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0" - } - } - export interface PostgresServerShowResult { apiVersion: string, // "arcdata.microsoft.com/v1alpha1" kind: string, // "postgresql-12" @@ -175,25 +163,56 @@ declare module 'azdata-ext' { uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0" }, spec: { - backups: { - deltaMinutes: number, // 3, - fullMinutes: number, // 10, - tiers: [ - { - retention: { - maximums: string[], // [ "6", "512MB" ], - minimums: string[], // [ "3" ] + engine: { + extensions: { + name: string // "citus" + }[], + settings: { + default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" } + } + }, + scale: { + shards: number // 1 + }, + scheduling: { + default: { + resources: { + requests: { + cpu: string, // "1.5" + memory: string // "256Mi" }, - storage: { - volumeSize: string, // "1Gi" + limits: { + cpu: string, // "1.5" + memory: string // "256Mi" } } - ] + } }, - status: { - readyPods: string, // "1/1", - state: string // "Ready" + service: { + type: string, // "NodePort" + port: number // 5432 + }, + storage: { + data: { + className: string, // "local-storage" + size: string // "5Gi" + }, + logs: { + className: string, // "local-storage" + size: string // "5Gi" + }, + backups: { + className: string, // "local-storage" + size: string // "5Gi" + } } + }, + status: { + externalEndpoint: string, // "10.130.12.136:26630" + readyPods: string, // "1/1", + state: string, // "Ready" + logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1')) + metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1 } } @@ -219,8 +238,25 @@ declare module 'azdata-ext' { }, postgres: { server: { + delete(name: string): Promise>, list(): Promise>, - show(name: string): Promise> + show(name: string): Promise>, + edit( + name: string, + args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + additionalEnvVars?: { [key: string]: string }): Promise> } }, sql: {