diff --git a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb index e51b8537d5..73f9a04f1d 100644 --- a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb +++ b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb @@ -25,17 +25,18 @@ "source": [ "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/master/src/sql/media/microsoft-small-logo.png)\n", " \n", - "## Create Azure Kubernetes Service cluster and deploy SQL Server 2019 big data cluster\n", + "## Create Azure Kubernetes Service cluster and deploy SQL Server 2019 Big Data Cluster\n", " \n", - "This notebook walks through the process of creating a new Azure Kubernetes Service cluster first, and then deploys a SQL Server 2019 big data cluster on the newly created AKS cluster.\n", + "This notebook walks through the process of creating a new Azure Kubernetes Service cluster first, and then deploys a SQL Server 2019 Big Data Cluster on the newly created AKS cluster.\n", " \n", "* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n", - "* The **Required information** cell will prompt you for a password that will be used to access the cluster controller, SQL Server, and Knox.\n", - "* The values in the **Azure settings** and **Default settings** cell can be changed as appropriate.\n", + "* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password will be used to access the cluster controller, SQL Server, and Knox.\n", "\n", "Please press the \"Run Cells\" button to run the notebook" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "4f6bc3bc-3592-420a-b534-384011189005" + } }, { "cell_type": "markdown", @@ -47,132 +48,87 @@ "|---|---|---|\n", "|Azure CLI |Command-line tool for managing Azure services. Used to create AKS cluster | [Installation](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) |\n", "|kubectl | Command-line tool for monitoring the underlying Kuberentes 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 a big data cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |" + "|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "d949980e-ad3f-4d02-ae84-7e4fbb19a087" + } }, { "cell_type": "markdown", - "source": "### **Check dependencies**", - "metadata": {} + "source": [ + "### **Check dependencies**" + ], + "metadata": { + "azdata_cell_guid": "a56d3413-a730-4997-b5c2-c8abd972757e" + } }, { "cell_type": "code", "source": [ - "import pandas,sys,os,getpass,time,json,html\r\n", + "import pandas,sys,os,json,html,getpass,time\r\n", "pandas_version = pandas.__version__.split('.')\r\n", "pandas_major = int(pandas_version[0])\r\n", "pandas_minor = int(pandas_version[1])\r\n", "pandas_patch = int(pandas_version[2])\r\n", "if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\r\n", " sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\r\n", - "\r\n", - "def run_command():\r\n", - " print(\"Executing: \" + cmd)\r\n", - " !{cmd}\r\n", + "def run_command(command):\r\n", + " print(\"Executing: \" + command)\r\n", + " !{command}\r\n", " if _exit_code != 0:\r\n", - " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{cmd}\\n')\r\n", - " print(f'Successfully executed: {cmd}')\r\n", + " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\r\n", + " print(f'Successfully executed: {command}')\r\n", "\r\n", - "cmd = 'az --version'\r\n", - "run_command()\r\n", - "cmd = 'kubectl version --client=true'\r\n", - "run_command()\r\n", - "cmd = 'azdata --version'\r\n", - "run_command()" + "run_command('kubectl version --client=true')\r\n", + "run_command('azdata --version')\r\n", + "run_command('az --version')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "326645cf-022a-47f2-8aff-37de71da8955" + }, "outputs": [], "execution_count": 1 }, { "cell_type": "markdown", - "source": "### **Required information**", - "metadata": {} + "source": [ + "### **Required information**" + ], + "metadata": { + "azdata_cell_guid": "720c200c-322a-49dd-9aa3-8bf7946aa251" + } }, { "cell_type": "code", "source": [ - "env_var_flag = \"AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD\" in os.environ\n", - "if env_var_flag:\n", - " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD\"]\n", - "else: \n", - " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 big data cluster controller password')\n", + "invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n", + "if invoked_by_wizard:\n", + " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n", + "else:\n", + " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n", " if mssql_password == \"\":\n", " sys.exit(f'Password is required.')\n", " confirm_password = getpass.getpass(prompt = 'Confirm password')\n", " if mssql_password != confirm_password:\n", " sys.exit(f'Passwords do not match.')\n", - "print('You can also use the same password to access Knox and SQL Server.')" + "print('You can also use the controller password to access Knox and SQL Server.')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "17e5d087-7128-4d02-8c16-fe1ddee675e5" + }, "outputs": [], "execution_count": 2 }, { "cell_type": "markdown", "source": [ - "### **Azure settings**\n", - "*Subscription ID*: visit here to find out the subscriptions you can use, if you leave it unspecified, the default subscription will be used.\n", - "\n", - "*VM Size*: visit here to find out the available VM sizes you could use. \n", - " \n", - "*Region*: visit here to find out the Azure regions where the Azure Kubernettes Service is available." + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" ], - "metadata": {} - }, - { - "cell_type": "code", - "source": [ - "if env_var_flag:\n", - " azure_subscription_id = os.environ[\"AZDATA_NB_VAR_BDC_AZURE_SUBSCRIPTION\"]\n", - " azure_vm_size = os.environ[\"AZDATA_NB_VAR_BDC_AZURE_VM_SIZE\"]\n", - " azure_region = os.environ[\"AZDATA_NB_VAR_BDC_AZURE_REGION\"]\n", - " azure_vm_count = int(os.environ[\"AZDATA_NB_VAR_BDC_VM_COUNT\"])\n", - "else:\n", - " azure_subscription_id = \"\"\n", - " azure_vm_size = \"Standard_E4s_v3\"\n", - " azure_region = \"eastus\"\n", - " azure_vm_count = int(5)" - ], - "metadata": {}, - "outputs": [], - "execution_count": 3 - }, - { - "cell_type": "markdown", - "source": "### **Default settings**", - "metadata": {} - }, - { - "cell_type": "code", - "source": [ - "if env_var_flag:\n", - " mssql_cluster_name = os.environ[\"AZDATA_NB_VAR_BDC_NAME\"]\n", - " mssql_controller_username = os.environ[\"AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME\"]\n", - " azure_resource_group = os.environ[\"AZDATA_NB_VAR_BDC_RESOURCEGROUP_NAME\"]\n", - " aks_cluster_name = os.environ[\"AZDATA_NB_VAR_BDC_AKS_NAME\"]\n", - "else:\n", - " mssql_cluster_name = 'mssql-cluster'\n", - " mssql_controller_username = 'admin'\n", - " azure_resource_group = mssql_cluster_name + '-' + time.strftime(\"%Y%m%d%H%M%S\", time.localtime())\n", - " aks_cluster_name = azure_resource_group\n", - "configuration_profile = 'aks-dev-test'\n", - "configuration_folder = 'mssql-bdc-configuration'\n", - "print(f'Azure subscription: {azure_subscription_id}')\n", - "print(f'Azure VM size: {azure_vm_size}')\n", - "print(f'Azure VM count: {str(azure_vm_count)}')\n", - "print(f'Azure region: {azure_region}')\n", - "print(f'Azure resource group: {azure_resource_group}')\n", - "print(f'AKS cluster name: {aks_cluster_name}')\n", - "print(f'SQL Server big data cluster name: {mssql_cluster_name}')\n", - "print(f'SQL Server big data cluster controller user name: {mssql_controller_username}')\n", - "print(f'Deployment configuration profile: {configuration_profile}')\n", - "print(f'Deployment configuration: {configuration_folder}')" - ], - "metadata": {}, - "outputs": [], - "execution_count": 4 + "metadata": { + "azdata_cell_guid": "4945bace-a50a-4e58-b55c-e9736106f805" + } }, { "cell_type": "markdown", @@ -182,15 +138,18 @@ "This will open a web browser window to enable credentials to be entered. If this cells is hanging forever, it might be because your Web browser windows is waiting for you to enter your Azure credentials!\n", "" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "baddf2d9-93ee-4c42-aaf1-b42116bb1912" + } }, { "cell_type": "code", "source": [ - "cmd = f'az login'\n", - "run_command()" + "run_command(f'az login')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "8f1404a6-216d-49fb-b6ad-81beeea50083" + }, "outputs": [], "execution_count": 5 }, @@ -200,125 +159,170 @@ "\n", "### **Set active Azure subscription**" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "230dc0f1-bf6e-474a-bfaa-aae6f8aad12e" + } }, { "cell_type": "code", "source": [ "if azure_subscription_id != \"\":\n", - " cmd = f'az account set --subscription {azure_subscription_id}'\n", - " run_command()\n", + " run_command(f'az account set --subscription {azure_subscription_id}')\n", "else:\n", " print('Using the default Azure subscription', {azure_subscription_id})\n", - "cmd = f'az account show'\n", - "run_command()" + "run_command(f'az account show')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "ab230931-2e99-483b-a229-3847684a8c1c" + }, "outputs": [], "execution_count": 6 }, { "cell_type": "markdown", - "source": "### **Create Azure resource group**", - "metadata": {} + "source": [ + "### **Create Azure resource group**" + ], + "metadata": { + "azdata_cell_guid": "d51db914-f484-489f-990d-72edb3065068" + } }, { "cell_type": "code", "source": [ - "cmd = f'az group create --name {azure_resource_group} --location {azure_region}'\n", - "run_command()" + "run_command(f'az group create --name {azure_resource_group} --location {azure_region}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "7c53eb23-c327-41bf-8936-bd34a02ebdd5" + }, "outputs": [], "execution_count": 7 }, { "cell_type": "markdown", - "source": "### **Create AKS cluster**", - "metadata": {} + "source": [ + "### **Create AKS cluster**" + ], + "metadata": { + "azdata_cell_guid": "818eb705-71e2-4013-8420-44886a5468b2" + } }, { "cell_type": "code", "source": [ - "cmd = f'az aks create --name {aks_cluster_name} --resource-group {azure_resource_group} --generate-ssh-keys --node-vm-size {azure_vm_size} --node-count {azure_vm_count}' \n", - "run_command()" + "run_command(f'az aks create --name {aks_cluster_name} --resource-group {azure_resource_group} --generate-ssh-keys --node-vm-size {azure_vm_size} --node-count {azure_vm_count}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "3cea1da0-0c18-4030-a5aa-79bc98a5a14d" + }, "outputs": [], "execution_count": 8 }, { "cell_type": "markdown", - "source": "### **Set the new AKS cluster as current context**", - "metadata": {} + "source": [ + "### **Set the new AKS cluster as current context**" + ], + "metadata": { + "azdata_cell_guid": "5ade8453-5e71-478f-b6b6-83c55626243d" + } }, { "cell_type": "code", "source": [ - "cmd = f'az aks get-credentials --resource-group {azure_resource_group} --name {aks_cluster_name} --admin --overwrite-existing'\r\n", - "run_command()" + "run_command(f'az aks get-credentials --resource-group {azure_resource_group} --name {aks_cluster_name} --admin --overwrite-existing')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "9ccb9adf-1cf6-4dcb-8bd9-7ae9a85c2437" + }, "outputs": [], "execution_count": 9 }, { "cell_type": "markdown", - "source": "### **Create a deployment configuration file**", - "metadata": {} + "source": [ + "### **Create deployment configuration files**" + ], + "metadata": { + "azdata_cell_guid": "57eb69fb-c68f-4ba8-818d-ffbaa0bc7aec" + } }, { "cell_type": "code", "source": [ "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", - "cmd = f'azdata bdc config init --source {configuration_profile} --target {configuration_folder} --force'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/bdc.json -j metadata.name={mssql_cluster_name}'\n", - "run_command()" + "mssql_target_profile = 'ads-bdc-custom-profile'\n", + "if not os.path.exists(mssql_target_profile):\n", + " os.mkdir(mssql_target_profile)\n", + "bdcJsonObj = json.loads(bdc_json)\n", + "controlJsonObj = json.loads(control_json)\n", + "bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n", + "bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n", + "bdcJsonFile.close()\n", + "controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n", + "controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n", + "controlJsonFile.close()\n", + "print(f'Created deployment configuration folder: {mssql_target_profile}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "3fd73c04-8a79-4d08-9049-1dad30265558" + }, "outputs": [], "execution_count": 10 }, { "cell_type": "markdown", - "source": "### **Create SQL Server 2019 big data cluster**", - "metadata": {} + "source": [ + "### **Create SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "6e82fad8-0fd0-4952-87ce-3fea1edd98cb" + } }, { "cell_type": "code", "source": [ - "print (f'Creating SQL Server 2019 big data cluster: {mssql_cluster_name} using configuration {configuration_folder}')\n", + "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", "os.environ[\"CONTROLLER_USERNAME\"] = mssql_controller_username\n", "os.environ[\"CONTROLLER_PASSWORD\"] = mssql_password\n", "os.environ[\"MSSQL_SA_PASSWORD\"] = mssql_password\n", "os.environ[\"KNOX_PASSWORD\"] = mssql_password\n", - "cmd = f'azdata bdc create -c {configuration_folder}'\n", - "run_command()" + "run_command(f'azdata bdc create -c {mssql_target_profile}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "c43ea026-ca5e-4e2a-8602-fcc786354168" + }, "outputs": [], "execution_count": 11 }, { "cell_type": "markdown", - "source": "### **Login to SQL Server 2019 big data cluster**", - "metadata": {} + "source": [ + "### **Login to SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "9c5428f4-08b9-4799-a35d-867c91dc29fb" + } }, { "cell_type": "code", "source": [ - "cmd = f'azdata login --cluster-name {mssql_cluster_name}'\n", - "run_command()" + "run_command(f'azdata login --cluster-name {mssql_cluster_name}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "5120c387-1088-435b-856e-e59f147c45a2" + }, "outputs": [], "execution_count": 12 }, { "cell_type": "markdown", - "source": "### **Show SQL Server 2019 big data cluster endpoints**", - "metadata": {} + "source": [ + "### **Show SQL Server 2019 Big Data Cluster endpoints**" + ], + "metadata": { + "azdata_cell_guid": "97974eda-e108-4c21-a58e-c6bb58f14ef1" + } }, { "cell_type": "code", @@ -332,17 +336,21 @@ "endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n", "display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "9a5d0aef-a8da-4845-b470-d714435f0304" + }, "outputs": [], "execution_count": 13 }, { "cell_type": "markdown", "source": [ - "### **Connect to master SQL Server instance in Azure Data Studio**\r\n", - "Click the link below to connect to the master SQL Server instance of the SQL Server 2019 big data cluster." + "### **Connect to SQL Server Master instance in Azure Data Studio**\r\n", + "Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster." ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "4a49b629-bd7a-43ba-bf18-6cdc0737b0f9" + } }, { "cell_type": "code", @@ -350,11 +358,13 @@ "sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\r\n", "if sqlEndpoints and len(sqlEndpoints) == 1:\r\n", " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(mssql_password) + '}'\r\n", - " display(HTML('
Click here to connect to master SQL Server instance
'))\r\n", + " display(HTML('
Click here to connect to SQL Server Master instance
'))\r\n", "else:\r\n", - " sys.exit('Could not find the master SQL Server instance endpoint.')" + " sys.exit('Could not find the SQL Server Master instance endpoint.')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "1c9d1f2c-62ba-4070-920a-d30b67bdcc7c" + }, "outputs": [], "execution_count": 14 } diff --git a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb index 59a0133d50..56d299b328 100644 --- a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb +++ b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb @@ -25,20 +25,18 @@ "source": [ "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/master/src/sql/media/microsoft-small-logo.png)\n", " \n", - "## Deploy SQL Server 2019 big data cluster on an existing Azure Kubernetes Service (AKS) cluster\n", + "## Deploy SQL Server 2019 Big Data Cluster on an existing Azure Kubernetes Service (AKS) cluster\n", " \n", - "This notebook walks through the process of deploying a SQL Server 2019 big data cluster on an existing AKS cluster.\n", + "This notebook walks through the process of deploying a SQL Server 2019 Big Data Cluster on an existing AKS cluster.\n", " \n", "* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n", - "* Make sure you have the target cluster set as the current context in your kubectl config file.\n", - " The config file would typically be under C:\\Users\\(userid)\\.kube on Windows, and under ~/.kube/ for macOS and Linux for a default installation.\n", - " In the kubectl config file, look for \"current-context\" and ensure it is set to the AKS cluster that the SQL Server 2019 big data cluster will be deployed to.\n", - "* The **Required information** cell will prompt you for password that will be used to access the cluster controller, SQL Server, and Knox.\n", - "* The values in the **Default settings** cell can be changed as appropriate.\n", + "* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password can be used to access the cluster controller, SQL Server, and Knox.\n", "\n", "Please press the \"Run Cells\" button to run the notebook" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "82e60c1a-7acf-47ee-877f-9e85e92e11da" + } }, { "cell_type": "markdown", @@ -49,14 +47,20 @@ "|Tools|Description|Installation|\n", "|---|---|---|\n", "|kubectl | Command-line tool for monitoring the underlying Kuberentes 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 a big data cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |" + "|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471" + } }, { "cell_type": "markdown", - "source": "### **Check dependencies**", - "metadata": {} + "source": [ + "### **Check dependencies**" + ], + "metadata": { + "azdata_cell_guid": "e3dd8e75-e15f-44b4-81fc-1f54d6f0b1e2" + } }, { "cell_type": "code", @@ -68,143 +72,168 @@ "pandas_patch = int(pandas_version[2])\r\n", "if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\r\n", " sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\r\n", - "def run_command():\r\n", - " print(\"Executing: \" + cmd)\r\n", - " !{cmd}\r\n", + "def run_command(command):\r\n", + " print(\"Executing: \" + command)\r\n", + " !{command}\r\n", " if _exit_code != 0:\r\n", - " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{cmd}\\n')\r\n", - " print(f'Successfully executed: {cmd}')\r\n", + " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\r\n", + " print(f'Successfully executed: {command}')\r\n", "\r\n", - "cmd = 'kubectl version --client=true'\r\n", - "run_command()\r\n", - "cmd = 'azdata --version'\r\n", - "run_command()" + "run_command('kubectl version --client=true')\r\n", + "run_command('azdata --version')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "d973d5b4-7f0a-4a9d-b204-a16480f3940d" + }, "outputs": [], "execution_count": 1 }, { "cell_type": "markdown", - "source": "### **Show current context**", - "metadata": {} - }, - { - "cell_type": "code", "source": [ - "cmd = ' kubectl config current-context'\r\n", - "run_command()" + "### **Required information**" ], - "metadata": {}, - "outputs": [], - "execution_count": 2 - }, - { - "cell_type": "markdown", - "source": "### **Required information**", - "metadata": {} + "metadata": { + "azdata_cell_guid": "0bb02e76-fee8-4dbc-a75b-d5b9d1b187d0" + } }, { "cell_type": "code", "source": [ - "env_var_flag = \"AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD\" in os.environ\n", - "if env_var_flag:\n", - " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD\"]\n", - "else: \n", - " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 big data cluster controller password')\n", + "invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n", + "if invoked_by_wizard:\n", + " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n", + "else:\n", + " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n", " if mssql_password == \"\":\n", " sys.exit(f'Password is required.')\n", " confirm_password = getpass.getpass(prompt = 'Confirm password')\n", " if mssql_password != confirm_password:\n", " sys.exit(f'Passwords do not match.')\n", - "print('You can also use the same password to access Knox and SQL Server.')" + "print('You can also use the controller password to access Knox and SQL Server.')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac" + }, "outputs": [], "execution_count": 3 }, { "cell_type": "markdown", - "source": "### **Default settings**", - "metadata": {} + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "c009edfe-b964-4b28-beeb-02a2c65f9918" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Set and show current context**" + ], + "metadata": { + "azdata_cell_guid": "127c8042-181f-4862-a390-96e59c181d09" + } }, { "cell_type": "code", "source": [ - "if env_var_flag:\n", - " mssql_cluster_name = os.environ[\"AZDATA_NB_VAR_BDC_NAME\"]\n", - " mssql_controller_username = os.environ[\"AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME\"]\n", - "else:\n", - " mssql_cluster_name = 'mssql-cluster'\n", - " mssql_controller_username = 'admin'\n", - "configuration_profile = 'aks-dev-test'\n", - "configuration_folder = 'mssql-bdc-configuration'\n", - "print(f'SQL Server big data cluster name: {mssql_cluster_name}')\n", - "print(f'SQL Server big data cluster controller user name: {mssql_controller_username}')\n", - "print(f'Deployment configuration profile: {configuration_profile}')\n", - "print(f'Deployment configuration: {configuration_folder}')" + "run_command(f'kubectl config use-context {mssql_cluster_context}')\n", + "run_command('kubectl config current-context')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "7d1a03d4-1df8-48eb-bff0-0042603b95b1" + }, "outputs": [], - "execution_count": 4 + "execution_count": 0 }, { "cell_type": "markdown", - "source": "### **Create a deployment configuration file**", - "metadata": {} + "source": [ + "### **Create deployment configuration files**" + ], + "metadata": { + "azdata_cell_guid": "138536c3-1db6-428f-9e5c-8269a02fb52e" + } }, { "cell_type": "code", "source": [ "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", - "cmd = f'azdata bdc config init --source {configuration_profile} --target {configuration_folder} --force'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/bdc.json -j metadata.name={mssql_cluster_name}'\n", - "run_command()" + "mssql_target_profile = 'ads-bdc-custom-profile'\n", + "if not os.path.exists(mssql_target_profile):\n", + " os.mkdir(mssql_target_profile)\n", + "bdcJsonObj = json.loads(bdc_json)\n", + "controlJsonObj = json.loads(control_json)\n", + "bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n", + "bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n", + "bdcJsonFile.close()\n", + "controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n", + "controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n", + "controlJsonFile.close()\n", + "print(f'Created deployment configuration folder: {mssql_target_profile}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "2ff82c8a-4bce-449c-9d91-3ac7dd272021" + }, "outputs": [], "execution_count": 6 }, { "cell_type": "markdown", - "source": "### **Create SQL Server 2019 big data cluster**", - "metadata": {} + "source": [ + "### **Create SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f" + } }, { "cell_type": "code", "source": [ - "print (f'Creating SQL Server 2019 big data cluster: {mssql_cluster_name} using configuration {configuration_folder}')\n", + "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", "os.environ[\"CONTROLLER_USERNAME\"] = mssql_controller_username\n", "os.environ[\"CONTROLLER_PASSWORD\"] = mssql_password\n", "os.environ[\"MSSQL_SA_PASSWORD\"] = mssql_password\n", "os.environ[\"KNOX_PASSWORD\"] = mssql_password\n", - "cmd = f'azdata bdc create -c {configuration_folder}'\n", - "run_command()" + "run_command(f'azdata bdc create -c {mssql_target_profile}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76" + }, "outputs": [], "execution_count": 7 }, { "cell_type": "markdown", - "source": "### **Login to SQL Server 2019 big data cluster**", - "metadata": {} + "source": [ + "### **Login to SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "4e026d39-12d4-4c80-8e30-de2b782f2110" + } }, { "cell_type": "code", "source": [ - "cmd = f'azdata login --cluster-name {mssql_cluster_name}'\n", - "run_command()" + "run_command(f'azdata login --cluster-name {mssql_cluster_name}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "79adda27-371d-4dcb-b867-db025f8162a5" + }, "outputs": [], "execution_count": 8 }, { "cell_type": "markdown", - "source": "### **Show SQL Server 2019 big data cluster endpoints**", - "metadata": {} + "source": [ + "### **Show SQL Server 2019 Big Data Cluster endpoints**" + ], + "metadata": { + "azdata_cell_guid": "c1921288-ad11-40d8-9aea-127a722b3df8" + } }, { "cell_type": "code", @@ -218,17 +247,21 @@ "endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n", "display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "a2202494-fd6c-4534-987d-15c403a5096f" + }, "outputs": [], "execution_count": 9 }, { "cell_type": "markdown", "source": [ - "### **Connect to master SQL Server instance in Azure Data Studio**\r\n", - "Click the link below to connect to the master SQL Server instance of the SQL Server 2019 big data cluster." + "### **Connect to SQL Server Master instance in Azure Data Studio**\r\n", + "Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster." ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "621863a2-aa61-46f4-a9d0-717f41c009ee" + } }, { "cell_type": "code", @@ -236,11 +269,13 @@ "sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\r\n", "if sqlEndpoints and len(sqlEndpoints) == 1:\r\n", " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(mssql_password) + '}'\r\n", - " display(HTML('
Click here to connect to master SQL Server instance
'))\r\n", + " display(HTML('
Click here to connect to SQL Server Master instance
'))\r\n", "else:\r\n", - " sys.exit('Could not find the master SQL Server instance endpoint')" + " sys.exit('Could not find the SQL Server Master instance endpoint')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "48342355-9d2b-4fa6-b1aa-3bc77d434dfa" + }, "outputs": [], "execution_count": 10 } diff --git a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb index 44a8263210..e0a9f7de8f 100644 --- a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb +++ b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb @@ -25,20 +25,18 @@ "source": [ "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/master/src/sql/media/microsoft-small-logo.png)\n", " \n", - "## Deploy SQL Server 2019 big data cluster on an existing cluster deployed using kubeadm\n", + "## Deploy SQL Server 2019 Big Data Cluster on an existing cluster deployed using kubeadm\n", " \n", - "This notebook walks through the process of deploying a SQL Server 2019 big data cluster on an existing kubeadm cluster.\n", + "This notebook walks through the process of deploying a SQL Server 2019 Big Data Cluster on an existing kubeadm cluster.\n", " \n", "* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n", - "* Make sure you have the target cluster set as the current context in your kubectl config file.\n", - " The config file would typically be under C:\\Users\\(userid)\\.kube on Windows, and under ~/.kube/ for macOS and Linux for a default installation.\n", - " In the kubectl config file, look for \"current-context\" and ensure it is set to the AKS cluster that the SQL Server 2019 big data cluster will be deployed to.\n", - "* The **Required information** cell will prompt you for password that will be used to access the cluster controller, SQL Server, and Knox.\n", - "* The values in the **Default settings** cell can be changed as appropriate.\n", + "* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password can be used to access the cluster controller, SQL Server, and Knox.\n", "\n", "Please press the \"Run Cells\" button to run the notebook" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "23954d96-3932-4a8e-ab73-da605f99b1a4" + } }, { "cell_type": "markdown", @@ -49,14 +47,20 @@ "|Tools|Description|Installation|\n", "|---|---|---|\n", "|kubectl | Command-line tool for monitoring the underlying Kuberentes 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 a big data cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |" + "|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |" ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "1d7f4c6a-0cb8-4ecc-81c8-544712253a3f" + } }, { "cell_type": "markdown", - "source": "### **Check dependencies**", - "metadata": {} + "source": [ + "### **Check dependencies**" + ], + "metadata": { + "azdata_cell_guid": "a31f9894-903f-4e19-a5a8-6fd888ff013b" + } }, { "cell_type": "code", @@ -68,165 +72,168 @@ "pandas_patch = int(pandas_version[2])\r\n", "if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\r\n", " sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\r\n", - "\r\n", - "def run_command():\r\n", - " print(\"Executing: \" + cmd)\r\n", - " !{cmd}\r\n", + "def run_command(command):\r\n", + " print(\"Executing: \" + command)\r\n", + " !{command}\r\n", " if _exit_code != 0:\r\n", - " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{cmd}\\n')\r\n", - " print(f'Successfully executed: {cmd}')\r\n", + " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\r\n", + " print(f'Successfully executed: {command}')\r\n", "\r\n", - "cmd = 'kubectl version --client=true'\r\n", - "run_command()\r\n", - "cmd = 'azdata --version'\r\n", - "run_command()" + "run_command('kubectl version --client=true')\r\n", + "run_command('azdata --version')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8" + }, "outputs": [], "execution_count": 1 }, { "cell_type": "markdown", - "source": "### **Show current context**", - "metadata": {} - }, - { - "cell_type": "code", "source": [ - "cmd = ' kubectl config current-context'\r\n", - "run_command()" + "### **Required information**" ], - "metadata": {}, - "outputs": [], - "execution_count": 2 - }, - { - "cell_type": "markdown", - "source": "### **Required information**", - "metadata": {} + "metadata": { + "azdata_cell_guid": "7b383b0d-5687-45b3-a16f-ba3b170c796e" + } }, { "cell_type": "code", "source": [ - "env_var_flag = \"AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD\" in os.environ\n", - "if env_var_flag:\n", - " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD\"]\n", - " mssql_storage_class = os.environ[\"AZDATA_NB_VAR_BDC_STORAGE_CLASS\"]\n", - " mssql_data_size = os.environ[\"AZDATA_NB_VAR_BDC_DATA_SIZE\"]\n", - " mssql_log_size = os.environ[\"AZDATA_NB_VAR_BDC_LOG_SIZE\"]\n", - "else: \n", - " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 big data cluster controller password')\n", + "invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n", + "if invoked_by_wizard:\n", + " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n", + "else:\n", + " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n", " if mssql_password == \"\":\n", " sys.exit(f'Password is required.')\n", " confirm_password = getpass.getpass(prompt = 'Confirm password')\n", " if mssql_password != confirm_password:\n", " sys.exit(f'Passwords do not match.')\n", - " mssql_storage_class = input('Storage class name')\n", - " mssql_data_size = input('Capacity for data in GB, default is 100GB')\n", - " if mssql_data_size == \"\":\n", - " mssql_data_size = \"100\"\n", - " mssql_log_size = input('Capacity for logs in GB, default is 50GB')\n", - " if mssql_log_size == \"\":\n", - " mssql_log_size = \"50\"\n", - "print('You can also use the same password to access Knox and SQL Server.')" + "print('You can also use the controller password to access Knox and SQL Server.')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e" + }, "outputs": [], "execution_count": 3 }, { "cell_type": "markdown", - "source": "### **Default settings**", - "metadata": {} + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "1d28aac5-955d-4b15-8b9c-8d6ec2b588fe" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Set and show current context**" + ], + "metadata": { + "azdata_cell_guid": "6456bd0c-5b64-4d76-be59-e3a5b32697f5" + } }, { "cell_type": "code", "source": [ - "if env_var_flag:\n", - " mssql_cluster_name = os.environ[\"AZDATA_NB_VAR_BDC_NAME\"]\n", - " mssql_controller_username = os.environ[\"AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME\"]\n", - "else:\n", - " mssql_cluster_name = 'mssql-cluster'\n", - " mssql_controller_username = 'admin'\n", - "configuration_profile = 'kubeadm-dev-test'\n", - "configuration_folder = 'mssql-bdc-configuration'\n", - "print(f'SQL Server big data cluster name: {mssql_cluster_name}')\n", - "print(f'SQL Server big data cluster controller user name: {mssql_controller_username}')\n", - "print(f'Storage class name: {mssql_storage_class}')\n", - "print(f'Capacity for data (GB): {mssql_data_size}')\n", - "print(f'Capacity for logs (GB): {mssql_log_size}')\n", - "print(f'Deployment configuration profile: {configuration_profile}')\n", - "print(f'Deployment configuration: {configuration_folder}')" + "run_command(f'kubectl config use-context {mssql_cluster_context}')\n", + "run_command('kubectl config current-context')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "a38f8b3a-f93a-484c-b9e2-4eba3ed99cc2" + }, "outputs": [], - "execution_count": 4 + "execution_count": 0 }, { "cell_type": "markdown", - "source": "### **Create a deployment configuration file**", - "metadata": {} + "source": [ + "### **Create deployment configuration files**" + ], + "metadata": { + "azdata_cell_guid": "6d78da36-6af5-4309-baad-bc81bb2cdb7f" + } }, { "cell_type": "code", "source": [ "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", - "cmd = f'azdata bdc config init --source {configuration_profile} --target {configuration_folder} --force'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/bdc.json -j metadata.name={mssql_cluster_name}'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/control.json -j $.spec.storage.data.className={mssql_storage_class}'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/control.json -j $.spec.storage.data.size={mssql_data_size}Gi'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/control.json -j $.spec.storage.logs.className={mssql_storage_class}'\n", - "run_command()\n", - "cmd = f'azdata bdc config replace -c {configuration_folder}/control.json -j $.spec.storage.logs.size={mssql_log_size}Gi'\n", - "run_command()" + "mssql_target_profile = 'ads-bdc-custom-profile'\n", + "if not os.path.exists(mssql_target_profile):\n", + " os.mkdir(mssql_target_profile)\n", + "bdcJsonObj = json.loads(bdc_json)\n", + "controlJsonObj = json.loads(control_json)\n", + "bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n", + "bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n", + "bdcJsonFile.close()\n", + "controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n", + "controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n", + "controlJsonFile.close()\n", + "print(f'Created deployment configuration folder: {mssql_target_profile}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf" + }, "outputs": [], "execution_count": 6 }, { "cell_type": "markdown", - "source": "### **Create SQL Server 2019 big data cluster**", - "metadata": {} + "source": [ + "### **Create SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "7d56d262-8cd5-49e4-b745-332c6e7a3cb2" + } }, { "cell_type": "code", "source": [ - "print (f'Creating SQL Server 2019 big data cluster: {mssql_cluster_name} using configuration {configuration_folder}')\n", + "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", "os.environ[\"CONTROLLER_USERNAME\"] = mssql_controller_username\n", "os.environ[\"CONTROLLER_PASSWORD\"] = mssql_password\n", "os.environ[\"MSSQL_SA_PASSWORD\"] = mssql_password\n", "os.environ[\"KNOX_PASSWORD\"] = mssql_password\n", - "cmd = f'azdata bdc create -c {configuration_folder}'\n", - "run_command()" + "run_command(f'azdata bdc create -c {mssql_target_profile}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b" + }, "outputs": [], "execution_count": 7 }, { "cell_type": "markdown", - "source": "### **Login to SQL Server 2019 big data cluster**", - "metadata": {} + "source": [ + "### **Login to SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "7929fd90-324d-482a-a101-ae29cb183691" + } }, { "cell_type": "code", "source": [ - "cmd = f'azdata login --cluster-name {mssql_cluster_name}'\n", - "run_command()" + "run_command(f'azdata login --cluster-name {mssql_cluster_name}')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "3a49909b-e09e-4e62-a825-c39de2cffc94" + }, "outputs": [], "execution_count": 8 }, { "cell_type": "markdown", - "source": "### **Show SQL Server 2019 big data cluster endpoints**", - "metadata": {} + "source": [ + "### **Show SQL Server 2019 Big Data Cluster endpoints**" + ], + "metadata": { + "azdata_cell_guid": "038e801a-a393-4f8d-8e2d-97bc3b740b0c" + } }, { "cell_type": "code", @@ -240,17 +247,21 @@ "endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n", "display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "2a8c8d5d-862c-4672-9309-38aa03afc4e6" + }, "outputs": [], "execution_count": 9 }, { "cell_type": "markdown", "source": [ - "### **Connect to master SQL Server instance in Azure Data Studio**\r\n", - "Click the link below to connect to the master SQL Server instance of the SQL Server 2019 big data cluster." + "### **Connect to SQL Server Master instance in Azure Data Studio**\r\n", + "Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster." ], - "metadata": {} + "metadata": { + "azdata_cell_guid": "0bd809fa-8225-4954-a50c-da57ea167896" + } }, { "cell_type": "code", @@ -258,11 +269,13 @@ "sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\r\n", "if sqlEndpoints and len(sqlEndpoints) == 1:\r\n", " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(mssql_password) + '}'\r\n", - " display(HTML('
Click here to connect to master SQL Server instance
'))\r\n", + " display(HTML('
Click here to connect to SQL Server Master instance
'))\r\n", "else:\r\n", - " sys.exit('Could not find the master SQL Server instance endpoint')" + " sys.exit('Could not find the SQL Server Master instance endpoint')" ], - "metadata": {}, + "metadata": { + "azdata_cell_guid": "d591785d-71aa-4c5d-9cbb-a7da79bca503" + }, "outputs": [], "execution_count": 10 } diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index f49e3190f3..0085c75731 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -234,8 +234,8 @@ "displayName": "%bdc-deployment-target%", "values": [ { - "name": "aks", - "displayName": "%bdc-deployment-target-aks%" + "name": "new-aks", + "displayName": "%bdc-deployment-target-new-aks%" }, { "name": "existing-aks", @@ -250,96 +250,9 @@ ], "providers": [ { - "dialog": { - "notebook": "%bdc-2019-aks-notebook%", - "title": "%bdc-new-aks-dialog-title%", - "name": "bdc-new-aks-dialog", - "tabs": [ - { - "title": "", - "sections": [ - { - "title": "%bdc-cluster-settings-section-title%", - "fields": [ - { - "label": "%bdc-cluster-name-field%", - "variableName": "AZDATA_NB_VAR_BDC_NAME", - "type": "text", - "defaultValue": "mssql-cluster", - "required": true - }, - { - "label": "%bdc-controller-username-field%", - "variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME", - "type": "text", - "defaultValue": "admin", - "required": true - }, - { - "label": "%bdc-password-field%", - "variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD", - "type": "sql_password", - "userName": "sa", - "confirmationRequired": true, - "confirmationLabel": "%bdc-confirm-password-field%", - "defaultValue": "", - "required": true - } - ] - }, - { - "title": "%bdc-azure-settings-section-title%", - "fields": [ - { - "label": "%bdc-azure-subscription-id-field%", - "variableName": "AZDATA_NB_VAR_BDC_AZURE_SUBSCRIPTION", - "type": "text", - "defaultValue": "", - "required": false, - "placeHolder": "%bdc-azure-subscription-id-placeholder%" - }, - { - "label": "%bdc-azure-resource-group-field%", - "variableName": "AZDATA_NB_VAR_BDC_RESOURCEGROUP_NAME", - "type": "datetime_text", - "defaultValue": "mssql-", - "required": true - }, - { - "label": "%bdc-azure-region-field%", - "variableName": "AZDATA_NB_VAR_BDC_AZURE_REGION", - "type": "text", - "defaultValue": "eastus", - "required": true - }, - { - "label": "%bdc-azure-aks-name-field%", - "variableName": "AZDATA_NB_VAR_BDC_AKS_NAME", - "type": "datetime_text", - "defaultValue": "mssql-", - "required": true - }, - { - "label": "%bdc-azure-vm-size-field%", - "variableName": "AZDATA_NB_VAR_BDC_AZURE_VM_SIZE", - "type": "text", - "defaultValue": "Standard_E4s_v3", - "required": true - }, - { - "label": "%bdc-azure-vm-count-field%", - "variableName": "AZDATA_NB_VAR_BDC_VM_COUNT", - "type": "number", - "defaultValue": "5", - "min": 1, - "max": 999, - "required": true - } - ] - } - ] - } - ] + "wizard": { + "type": "new-aks", + "notebook": "%bdc-2019-aks-notebook%" }, "requiredTools": [ { @@ -352,49 +265,12 @@ "name": "azdata" } ], - "when": "target=aks&&version=bdc2019" + "when": "target=new-aks&&version=bdc2019" }, { - "dialog": { - "notebook": "%bdc-2019-existing-aks-notebook%", - "title": "%bdc-existing-aks-dialog-title%", - "name": "bdc-existing-aks-dialog", - "tabs": [ - { - "title": "", - "sections": [ - { - "title": "%bdc-cluster-settings-section-title%", - "fields": [ - { - "label": "%bdc-cluster-name-field%", - "variableName": "AZDATA_NB_VAR_BDC_NAME", - "type": "text", - "defaultValue": "mssql-cluster", - "required": true - }, - { - "label": "%bdc-controller-username-field%", - "variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME", - "type": "text", - "defaultValue": "admin", - "required": true - }, - { - "label": "%bdc-password-field%", - "variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD", - "type": "sql_password", - "userName": "sa", - "confirmationRequired": true, - "confirmationLabel": "%bdc-confirm-password-field%", - "defaultValue": "", - "required": true - } - ] - } - ] - } - ] + "wizard": { + "type": "existing-aks", + "notebook": "%bdc-2019-existing-aks-notebook%" }, "requiredTools": [ { @@ -407,69 +283,9 @@ "when": "target=existing-aks&&version=bdc2019" }, { - "dialog": { - "title": "%bdc-existing-kubeadm-dialog-title%", - "name": "bdc-existing-kubeadm-dialog", - "notebook": "%bdc-2019-existing-kubeadm-notebook%", - "tabs": [ - { - "title": "", - "sections": [ - { - "title": "%bdc-cluster-settings-section-title%", - "fields": [ - { - "label": "%bdc-cluster-name-field%", - "variableName": "AZDATA_NB_VAR_BDC_NAME", - "type": "text", - "defaultValue": "mssql-cluster", - "required": true - }, - { - "label": "%bdc-controller-username-field%", - "variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME", - "type": "text", - "defaultValue": "admin", - "required": true - }, - { - "label": "%bdc-password-field%", - "variableName": "AZDATA_NB_VAR_BDC_CONTROLLER_PASSWORD", - "type": "sql_password", - "userName": "sa", - "confirmationRequired": true, - "confirmationLabel": "%bdc-confirm-password-field%", - "defaultValue": "", - "required": true - }, - { - "label": "%bdc-storage-class-field%", - "variableName": "AZDATA_NB_VAR_BDC_STORAGE_CLASS", - "type": "text", - "defaultValue": "", - "required": true - }, - { - "label": "%bdc-data-size-field%", - "variableName": "AZDATA_NB_VAR_BDC_DATA_SIZE", - "type": "number", - "defaultValue": "100", - "min": 1, - "required": true - }, - { - "label": "%bdc-log-size-field%", - "variableName": "AZDATA_NB_VAR_BDC_LOG_SIZE", - "type": "number", - "defaultValue": "50", - "min": 1, - "required": true - } - ] - } - ] - } - ] + "wizard": { + "type": "existing-kubeadm", + "notebook": "%bdc-2019-existing-kubeadm-notebook%" }, "requiredTools": [ { @@ -481,7 +297,24 @@ ], "when": "target=existing-kubeadm&&version=bdc2019" } - ] + ], + "agreement": { + "template": "%bdc-agreement%", + "links": [ + { + "text": "%bdc-agreement-privacy-statement%", + "url": "https://go.microsoft.com/fwlink/?LinkId=853010" + }, + { + "text": "%bdc-agreement-bdc-eula%", + "url": "https://go.microsoft.com/fwlink/?LinkId=2002534" + }, + { + "text": "%bdc-agreement-azdata-eula%", + "url": "https://aka.ms/azdata-eula" + } + ] + } }, { "name": "sql-windows-setup", @@ -525,10 +358,12 @@ } ], "dependencies": { - "vscode-nls": "^4.0.0" + "vscode-nls": "^4.0.0", + "yamljs": "^0.3.0" }, "devDependencies": { "typemoq": "^2.1.0", - "vscode": "^1.1.26" + "vscode": "^1.1.26", + "@types/yamljs": "0.2.30" } } diff --git a/extensions/resource-deployment/package.nls.json b/extensions/resource-deployment/package.nls.json index c78b96bf08..e7bb019eff 100644 --- a/extensions/resource-deployment/package.nls.json +++ b/extensions/resource-deployment/package.nls.json @@ -2,21 +2,21 @@ "extension-displayName": "SQL Server Deployment extension for Azure Data Studio", "extension-description": "Provides a notebook-based experience to deploy Microsoft SQL Server", "deploy-sql-image-command-name": "Deploy SQL Server on Docker…", - "deploy-sql-bdc-command-name": "Deploy SQL Server big data cluster…", + "deploy-sql-bdc-command-name": "Deploy SQL Server Big Data Cluster…", "deploy-resource-command-name": "Open SQL Server deployment dialog", "deploy-resource-command-category": "Deployment", "resource-type-sql-image-display-name": "SQL Server container image", "resource-type-sql-image-description": "Run SQL Server container image with Docker", - "resource-type-sql-bdc-display-name": "SQL Server big data cluster", - "resource-type-sql-bdc-description": "SQL Server big data cluster allows you to deploy scalable clusters of SQL Server, Spark, and HDFS containers running on Kubernetes", + "resource-type-sql-bdc-display-name": "SQL Server Big Data Cluster", + "resource-type-sql-bdc-description": "SQL Server Big Data Cluster allows you to deploy scalable clusters of SQL Server, Spark, and HDFS containers running on Kubernetes", "version-display-name": "Version", "sql-2017-display-name": "SQL Server 2017", "sql-2019-display-name": "SQL Server 2019 RC", "sql-2017-docker-notebook": "./notebooks/docker/2017/deploy-sql2017-image.ipynb", "sql-2019-docker-notebook": "./notebooks/docker/2019/deploy-sql2019-image.ipynb", - "bdc-2019-display-name": "SQL Server 2019 RC big data cluster", + "bdc-2019-display-name": "SQL Server 2019 RC Big Data Cluster", "bdc-deployment-target": "Deployment target", - "bdc-deployment-target-aks": "New Azure Kubernetes Service Cluster", + "bdc-deployment-target-new-aks": "New Azure Kubernetes Service Cluster", "bdc-deployment-target-existing-aks": "Existing Azure Kubernetes Service Cluster", "bdc-deployment-target-existing-kubeadm": "Existing Kubernetes Cluster (kubeadm)", "bdc-2019-aks-notebook": "./notebooks/bdc/2019/deploy-bdc-aks.ipynb", @@ -30,7 +30,7 @@ "docker-sql-port-field": "Port", "bdc-new-aks-dialog-title": "Deployment target: new AKS cluster", "bdc-existing-aks-dialog-title": "Deployment target: existing AKS cluster", - "bdc-cluster-settings-section-title": "SQL Server big data cluster settings", + "bdc-cluster-settings-section-title": "SQL Server Big Data Cluster settings", "bdc-cluster-name-field": "Cluster name", "bdc-controller-username-field": "Controller username", "bdc-password-field": "Password", @@ -48,5 +48,9 @@ "bdc-data-size-field": "Capacity for data (GB)", "bdc-log-size-field": "Capacity for logs (GB)", "resource-type-sql-windows-setup-display-name": "SQL Server on Windows", - "resource-type-sql-windows-setup-description": "Run SQL Server on Windows, select a version to get started." + "resource-type-sql-windows-setup-description": "Run SQL Server on Windows, select a version to get started.", + "bdc-agreement": "I accept {0}, {1} and {2}.", + "bdc-agreement-privacy-statement":"Microsoft Privacy Statement", + "bdc-agreement-azdata-eula":"azdata License Terms", + "bdc-agreement-bdc-eula":"SQL Server Big Data Cluster License Terms" } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index d0fbc58c88..355378da4e 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -2,7 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; +import * as azdata from 'azdata'; +import { SemVer } from 'semver'; + +export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_'; export interface ResourceType { name: string; @@ -12,9 +15,15 @@ export interface ResourceType { icon: { light: string; dark: string }; options: ResourceTypeOption[]; providers: DeploymentProvider[]; + agreement?: AgreementInfo; getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined; } +export interface AgreementInfo { + template: string; + links: azdata.LinkArea[]; +} + export interface ResourceTypeOption { name: string; displayName: string; @@ -32,10 +41,16 @@ export interface DeploymentProvider { notebook: string | NotebookInfo; downloadUrl: string; webPageUrl: string; + wizard: WizardInfo; requiredTools: ToolRequirementInfo[]; when: string; } +export interface WizardInfo { + notebook: string | NotebookInfo; + type: BdcDeploymentType; +} + export interface DialogInfo { notebook: string | NotebookInfo; title: string; @@ -45,27 +60,56 @@ export interface DialogInfo { export interface DialogTabInfo { title: string; - sections: DialogSectionInfo[]; + sections: SectionInfo[]; + labelWidth?: string; + inputWidth?: string; } -export interface DialogSectionInfo { +export interface SectionInfo { title: string; - fields: DialogFieldInfo[]; + fields?: FieldInfo[]; // Use this if the dialog is not wide. All fields will be displayed in one column, label will be placed on top of the input component. + rows?: RowInfo[]; // Use this for wide dialog or wizard. label will be placed to the left of the input component. + labelWidth?: string; + inputWidth?: string; + labelPosition?: LabelPosition; // Default value is top + collapsible?: boolean; + collapsed?: boolean; + spaceBetweenFields?: string; } -export interface DialogFieldInfo { +export interface RowInfo { + fields: FieldInfo[]; +} + +export interface FieldInfo { label: string; - variableName: string; + variableName?: string; type: FieldType; - defaultValue: string; - confirmationRequired: boolean; - confirmationLabel: string; + defaultValue?: string; + confirmationRequired?: boolean; + confirmationLabel?: string; min?: number; max?: number; - required: boolean; - options: string[]; - placeHolder: string; - userName?: string; //needed for sql server's password complexity requirement check, password can not include the login name. + required?: boolean; + options?: string[] | azdata.CategoryValue[]; + placeHolder?: string; + userName?: string; // needed for sql server's password complexity requirement check, password can not include the login name. + labelWidth?: string; + inputWidth?: string; + description?: string; + useCustomValidator?: boolean; + labelPosition?: LabelPosition; // overwrite the labelPosition of SectionInfo. + fontStyle?: FontStyle; +} + +export enum LabelPosition { + Top = 'top', + Left = 'left' +} + +export enum FontStyle { + Normal = 'normal', + Italic = 'italic' } export enum FieldType { @@ -74,7 +118,9 @@ export enum FieldType { DateTimeText = 'datetime_text', SQLPassword = 'sql_password', Password = 'password', - Options = 'options' + Options = 'options', + ReadonlyText = 'readonly_text', + Checkbox = 'checkbox' } export interface NotebookInfo { @@ -100,4 +146,15 @@ export interface ITool { readonly displayName: string; readonly description: string; readonly type: ToolType; + readonly version: SemVer | undefined; + readonly homePage: string; + readonly isInstalled: boolean; + loadInformation(): Promise; + readonly statusDescription: string | undefined; +} + +export enum BdcDeploymentType { + NewAKS = 'new-aks', + ExistingAKS = 'existing-aks', + ExistingKubeAdm = 'existing-kubeadm' } diff --git a/extensions/resource-deployment/src/main.ts b/extensions/resource-deployment/src/main.ts index 36f7f0ff6f..f20d31c352 100644 --- a/extensions/resource-deployment/src/main.ts +++ b/extensions/resource-deployment/src/main.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; @@ -17,8 +16,8 @@ import { ResourceTypePickerDialog } from './ui/resourceTypePickerDialog'; const localize = nls.loadMessageBundle(); export function activate(context: vscode.ExtensionContext) { - const platformService = new PlatformService(); - const toolsService = new ToolsService(); + const platformService = new PlatformService(context.globalStoragePath); + const toolsService = new ToolsService(platformService); const notebookService = new NotebookService(platformService, context.extensionPath); const resourceTypeService = new ResourceTypeService(platformService, toolsService, notebookService); const resourceTypes = resourceTypeService.getResourceTypes(); diff --git a/extensions/resource-deployment/src/services/azdataService.ts b/extensions/resource-deployment/src/services/azdataService.ts new file mode 100644 index 0000000000..05f99e92a9 --- /dev/null +++ b/extensions/resource-deployment/src/services/azdataService.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import { IPlatformService } from './platformService'; +import { BigDataClusterDeploymentProfile } from './bigDataClusterDeploymentProfile'; + +interface BdcConfigListOutput { + stdout: string[]; +} + +export interface IAzdataService { + getDeploymentProfiles(): Promise; +} + +export class AzdataService implements IAzdataService { + constructor(private platformService: IPlatformService) { + } + + public async getDeploymentProfiles(): Promise { + await this.ensureWorkingDirectoryExists(); + const profileNames = await this.getDeploymentProfileNames(); + return await Promise.all(profileNames.map(profile => this.getDeploymentProfileInfo(profile))); + } + + private async getDeploymentProfileNames(): Promise { + const env: NodeJS.ProcessEnv = {}; + // azdata requires this environment variables to be set + env['ACCEPT_EULA'] = 'yes'; + const cmd = 'azdata bdc config list -o json'; + // Run the command twice to workaround the issue: + // First time use of the azdata will have extra EULA related string in the output + // there is no easy and reliable way to filter out the profile names from it. + await this.platformService.runCommand(cmd, { additionalEnvironmentVariables: env }); + const stdout = await this.platformService.runCommand(cmd); + const output = JSON.parse(stdout); + return output.stdout; + } + + private async getDeploymentProfileInfo(profileName: string): Promise { + await this.platformService.runCommand(`azdata bdc config init --source ${profileName} --target ${profileName} --force`, { workingDirectory: this.platformService.storagePath() }); + const configObjects = await Promise.all([ + this.getJsonObjectFromFile(path.join(this.platformService.storagePath(), profileName, 'bdc.json')), + this.getJsonObjectFromFile(path.join(this.platformService.storagePath(), profileName, 'control.json')) + ]); + return new BigDataClusterDeploymentProfile(profileName, configObjects[0], configObjects[1]); + } + + private async ensureWorkingDirectoryExists(): Promise { + if (! await this.platformService.fileExists(this.platformService.storagePath())) { + await this.platformService.makeDirectory(this.platformService.storagePath()); + } + } + + private async getJsonObjectFromFile(path: string): Promise { + return JSON.parse(await this.platformService.readTextFile(path)); + } +} diff --git a/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts b/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts new file mode 100644 index 0000000000..3dcf9c9a54 --- /dev/null +++ b/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const SqlServerMasterResource = 'master'; +export const DataResource = 'data-0'; +export const HdfsResource = 'storage-0'; +export const ComputeResource = 'compute-0'; +export const NameNodeResource = 'nmnode-0'; +export const SparkHeadResource = 'sparkhead'; +export const ZooKeeperResource = 'zookeeper'; +export const SparkResource = 'spark-0'; +export const HadrEnabledSetting = 'hadr.enabled'; + +interface ServiceEndpoint { + port: number; + serviceType: ServiceType; + name: EndpointName; +} +type ServiceType = 'NodePort' | 'LoadBalancer'; +type EndpointName = 'Controller' | 'Master' | 'Knox' | 'MasterSecondary'; + +export class BigDataClusterDeploymentProfile { + constructor(private _profileName: string, private _bdcConfig: any, private _controlConfig: any) { + // TODO: add validation logic for these 2 objects + // https://github.com/microsoft/azuredatastudio/issues/7344 + } + + public get profileName(): string { + return this._profileName; + } + + public get clusterName(): string { + return this._bdcConfig.metadata.name; + } + + public set clusterName(value: string) { + this._bdcConfig.metadata.name = value; + } + + public get bdcConfig(): any { + return this._bdcConfig; + } + + public get controlConfig(): any { + return this._controlConfig; + } + + public get sqlServerReplicas(): number { + return this.getReplicas(SqlServerMasterResource); + } + + public set sqlServerReplicas(replicas: number) { + this.setReplicas(SqlServerMasterResource, replicas); + } + + public get hdfsNameNodeReplicas(): number { + return this.getReplicas(NameNodeResource); + } + + public set hdfsNameNodeReplicas(replicas: number) { + this.setReplicas(NameNodeResource, replicas); + } + + public get sparkHeadReplicas(): number { + return this.getReplicas(SparkHeadResource); + } + + public set sparkHeadReplicas(replicas: number) { + this.setReplicas(SparkHeadResource, replicas); + } + + public get dataReplicas(): number { + return this.getReplicas(DataResource); + } + + public set dataReplicas(replicas: number) { + this.setReplicas(SparkHeadResource, replicas); + } + + public get hdfsReplicas(): number { + return this.getReplicas(HdfsResource); + } + + public set hdfsReplicas(replicas: number) { + this.setReplicas(HdfsResource, replicas); + } + + public get zooKeeperReplicas(): number { + return this.getReplicas(ZooKeeperResource); + } + + public set zooKeeperReplicas(replicas: number) { + this.setReplicas(ZooKeeperResource, replicas); + } + + public get computeReplicas(): number { + return this.getReplicas(ComputeResource); + } + + public set computeReplicas(replicas: number) { + this.setReplicas(ComputeResource, replicas); + } + + public get sparkReplicas(): number { + return this._bdcConfig.spec.resources[SparkResource] ? this.getReplicas(SparkResource) : 0; + } + + public get hadrEnabled(): boolean { + const value = this._bdcConfig.spec.resources[SqlServerMasterResource].spec.settings.sql[HadrEnabledSetting]; + return value === true || value === 'true'; + } + + public set hadrEnabled(value: boolean) { + this._bdcConfig.spec.resources[SqlServerMasterResource].spec.settings.sql[HadrEnabledSetting] = value; + } + + public get includeSpark(): boolean { + return this._bdcConfig.spec.resources[HdfsResource].spec.settings.spark.includeSpark; + } + + public set includeSpark(value: boolean) { + this._bdcConfig.spec.resources[HdfsResource].spec.settings.spark.includeSpark = value; + } + + public get controllerDataStorageClass(): string { + return this._controlConfig.spec.storage.data.className; + } + + public set controllerDataStorageClass(value: string) { + this._controlConfig.spec.storage.data.className = value; + } + + public get controllerDataStorageSize(): number { + return this._controlConfig.spec.storage.data.size.replace('Gi', ''); + } + + public set controllerDataStorageSize(value: number) { + this._controlConfig.spec.storage.data.size = value; + } + + public get controllerLogsStorageClass(): string { + return this._controlConfig.spec.storage.logs.className; + } + + public set controllerLogsStorageClass(value: string) { + this._controlConfig.spec.storage.logs.className = value; + } + + public get controllerLogsStorageSize(): number { + return this._controlConfig.spec.storage.logs.size.replace('Gi', ''); + } + + public set controllerLogsStorageSize(value: number) { + this._controlConfig.spec.storage.logs.size = value; + } + + public setResourceStorage(resourceName: 'data-0' | 'master' | 'storage-0', dataStorageClass: string, dataStorageSize: number, logsStorageClass: string, logsStorageSize: number) { + this.bdcConfig.spec.resources[resourceName]['storage'] = { + data: { + size: `${dataStorageSize}Gi`, + className: dataStorageClass, + accessMode: 'ReadWriteOnce' + }, + logs: { + size: `${logsStorageSize}Gi`, + className: logsStorageClass, + accessMode: 'ReadWriteOnce' + } + }; + } + + public get controllerPort(): number { + return this.getEndpointPort(this._controlConfig.spec.endpoints, 'Controller', 30080); + } + + public set controllerPort(port: number) { + this.setEndpointPort(this._controlConfig.spec.endpoints, 'Controller', port); + } + + public get sqlServerPort(): number { + return this.getEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', 31433); + } + + public set sqlServerPort(port: number) { + this.setEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', port); + } + + public get sqlServerReadableSecondaryPort(): number { + return this.getEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', 31436); + } + + public set sqlServerReadableSecondaryPort(port: number) { + this.setEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', port); + } + + public get gatewayPort(): number { + return this.getEndpointPort(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', 30443); + } + + public set gatewayPort(port: number) { + this.setEndpointPort(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', port); + } + + public addSparkResource(replicas: number): void { + this._bdcConfig.spec.resources[SparkResource] = { + metadata: { + kind: 'Pool', + name: 'default' + }, + spec: { + type: 'Spark', + replicas: replicas + } + }; + + this._bdcConfig.spec.services.spark.resources.push(SparkResource); + this._bdcConfig.spec.services.hdfs.resources.push(SparkResource); + } + + public get activeDirectorySupported(): boolean { + // TODO: Implement AD authentication + return false; + } + + public getBdcJson(readable: boolean = true): string { + return this.stringifyJson(this._bdcConfig, readable); + } + + public getControlJson(readable: boolean = true): string { + return this.stringifyJson(this._controlConfig, readable); + } + + private stringifyJson(obj: any, readable: boolean): string { + return JSON.stringify(obj, undefined, readable ? 4 : 0); + } + + private getReplicas(resourceName: string): number { + return this._bdcConfig.spec.resources[resourceName].spec.replicas; + } + + private setReplicas(resourceName: string, replicas: number): void { + this._bdcConfig.spec.resources[resourceName].spec.replicas = replicas; + } + + private getEndpointPort(endpoints: ServiceEndpoint[], name: EndpointName, defaultValue: number): number { + const endpoint = endpoints.find(endpoint => endpoint.name === name); + return endpoint ? endpoint.port : defaultValue; + } + + private setEndpointPort(endpoints: ServiceEndpoint[], name: EndpointName, port: number): void { + const endpoint = endpoints.find(endpoint => endpoint.name === name); + if (endpoint) { + endpoint.port = port; + } else { + endpoints.push({ + name: name, + serviceType: 'NodePort', + port: port + }); + } + } +} diff --git a/extensions/resource-deployment/src/services/kubeService.ts b/extensions/resource-deployment/src/services/kubeService.ts new file mode 100644 index 0000000000..a728ffe937 --- /dev/null +++ b/extensions/resource-deployment/src/services/kubeService.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as os from 'os'; +import * as yamljs from 'yamljs'; +import * as fs from 'fs'; + +export interface KubeClusterContext { + name: string; + isCurrentContext: boolean; +} + +export interface IKubeService { + getDefautConfigPath(): string; + getClusterContexts(configFile: string): Promise; +} + +export class KubeService implements IKubeService { + getDefautConfigPath(): string { + return path.join(os.homedir(), '.kube', 'config'); + } + + getClusterContexts(configFile: string): Promise { + return fs.promises.access(configFile).catch((error) => { + if (error && error.code === 'ENOENT') { + return []; + } else { + throw error; + } + }).then(() => { + const config = yamljs.load(configFile); + const rawContexts = config['contexts']; + const currentContext = config['current-context']; + const contexts: KubeClusterContext[] = []; + if (currentContext && rawContexts && rawContexts.length > 0) { + rawContexts.forEach(rawContext => { + const name = rawContext['name']; + if (name) { + contexts.push({ + name: name, + isCurrentContext: name === currentContext + }); + } + }); + } + return contexts; + }); + } +} diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 246ffda4a9..29d6d5e9f9 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; import * as path from 'path'; @@ -14,7 +13,7 @@ import { NotebookInfo } from '../interfaces'; const localize = nls.loadMessageBundle(); export interface INotebookService { - launchNotebook(notebook: string | NotebookInfo): void; + launchNotebook(notebook: string | NotebookInfo): Thenable; } export class NotebookService implements INotebookService { @@ -25,18 +24,22 @@ export class NotebookService implements INotebookService { * Copy the notebook to the user's home directory and launch the notebook from there. * @param notebook the path of the notebook */ - launchNotebook(notebook: string | NotebookInfo): void { + launchNotebook(notebook: string | NotebookInfo): Thenable { const notebookPath = this.getNotebook(notebook); const notebookFullPath = path.join(this.extensionPath, notebookPath); - if (notebookPath && this.platformService.fileExists(notebookPath)) { - this.showNotebookAsUntitled(notebookPath); - } - else if (notebookPath && this.platformService.fileExists(notebookFullPath)) { - this.showNotebookAsUntitled(notebookFullPath); - } - else { - this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath)); - } + return this.platformService.fileExists(notebookPath).then((notebookPathExists) => { + if (notebookPathExists) { + return this.showNotebookAsUntitled(notebookPath); + } else { + return this.platformService.fileExists(notebookFullPath).then(notebookFullPathExists => { + if (notebookFullPathExists) { + return this.showNotebookAsUntitled(notebookFullPath); + } else { + throw localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath); + } + }); + } + }); } /** @@ -74,12 +77,12 @@ export class NotebookService implements INotebookService { return title; } - showNotebookAsUntitled(notebookPath: string): void { + showNotebookAsUntitled(notebookPath: string): Thenable { let targetFileName: string = this.findNextUntitledEditorName(notebookPath); const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`); - vscode.workspace.openTextDocument(notebookPath).then((document) => { + return vscode.workspace.openTextDocument(notebookPath).then((document) => { let initialContent = document.getText(); - azdata.nb.showNotebookDocument(untitledFileName, { + return azdata.nb.showNotebookDocument(untitledFileName, { connectionProfile: undefined, preview: false, initialContent: initialContent, diff --git a/extensions/resource-deployment/src/services/platformService.ts b/extensions/resource-deployment/src/services/platformService.ts index aef5e1778d..05f18b792b 100644 --- a/extensions/resource-deployment/src/services/platformService.ts +++ b/extensions/resource-deployment/src/services/platformService.ts @@ -2,37 +2,58 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as fs from 'fs'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; +import * as cp from 'child_process'; /** * Abstract of platform dependencies */ export interface IPlatformService { platform(): string; - copyFile(source: string, target: string): void; - fileExists(file: string): boolean; + storagePath(): string; + copyFile(source: string, target: string): Promise; + fileExists(file: string): Promise; openFile(filePath: string): void; showErrorMessage(message: string): void; isNotebookNameUsed(title: string): boolean; + makeDirectory(path: string): Promise; + readTextFile(filePath: string): Promise; + runCommand(command: string, options?: CommandOptions): Promise; +} + +export interface CommandOptions { + workingDirectory?: string; + additionalEnvironmentVariables?: NodeJS.ProcessEnv; } export class PlatformService implements IPlatformService { + constructor(private _storagePath: string = '') { + } + + storagePath(): string { + return this._storagePath; + } + platform(): string { return process.platform; } - copyFile(source: string, target: string): void { - // tslint:disable-next-line:no-sync - fs.copyFileSync(source, target); + copyFile(source: string, target: string): Promise { + return fs.promises.copyFile(source, target); } - fileExists(file: string): boolean { - // tslint:disable-next-line:no-sync - return fs.existsSync(file); + fileExists(file: string): Promise { + return fs.promises.access(file).then(() => { + return true; + }).catch(error => { + if (error && error.code === 'ENOENT') { + return false; + } + throw error; + }); } openFile(filePath: string): void { @@ -46,4 +67,28 @@ export class PlatformService implements IPlatformService { isNotebookNameUsed(title: string): boolean { return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1); } + + makeDirectory(path: string): Promise { + return fs.promises.mkdir(path); + } + + readTextFile(filePath: string): Promise { + return fs.promises.readFile(filePath, 'utf8'); + } + + runCommand(command: string, options?: CommandOptions): Promise { + return new Promise((resolve, reject) => { + const env = Object.assign({}, process.env, options && options.additionalEnvironmentVariables); + cp.exec(command, { + cwd: options && options.workingDirectory, + env: env + }, (error, stdout, stderror) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); + } } diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index f72db5f5ea..dbbaac0b1b 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; import * as cp from 'child_process'; @@ -16,7 +15,10 @@ import { INotebookService } from './notebookService'; import { IPlatformService } from './platformService'; import { IToolsService } from './toolsService'; import { ResourceType, ResourceTypeOption, DeploymentProvider } from '../interfaces'; +import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; import { NotebookInputDialog } from '../ui/notebookInputDialog'; +import { KubeService } from './kubeService'; +import { AzdataService } from './azdataService'; const localize = nls.loadMessageBundle(); export interface IResourceTypeService { @@ -138,7 +140,7 @@ export class ResourceTypeService implements IResourceTypeService { let providerIndex = 1; resourceType.providers.forEach(provider => { const providerPositionInfo = `${positionInfo}, provider index: ${providerIndex} `; - if (!provider.dialog && !provider.notebook && !provider.downloadUrl && !provider.webPageUrl) { + if (!provider.wizard && !provider.dialog && !provider.notebook && !provider.downloadUrl && !provider.webPageUrl) { errorMessages.push(`No deployment method defined for the provider, ${providerPositionInfo}`); } @@ -195,7 +197,10 @@ export class ResourceTypeService implements IResourceTypeService { public startDeployment(provider: DeploymentProvider): void { const self = this; - if (provider.dialog) { + if (provider.wizard) { + const wizard = new DeployClusterWizard(provider.wizard, new KubeService(), new AzdataService(this.platformService), this.notebookService); + wizard.open(); + } else if (provider.dialog) { const dialog = new NotebookInputDialog(this.notebookService, provider.dialog); dialog.open(); } else if (provider.notebook) { diff --git a/extensions/resource-deployment/src/services/tools/azCliTool.ts b/extensions/resource-deployment/src/services/tools/azCliTool.ts index 0f5e9a69c5..ee0325319f 100644 --- a/extensions/resource-deployment/src/services/tools/azCliTool.ts +++ b/extensions/resource-deployment/src/services/tools/azCliTool.ts @@ -2,12 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { ToolType, ITool } from '../../interfaces'; + +import { ToolType } from '../../interfaces'; import * as nls from 'vscode-nls'; +import { SemVer } from 'semver'; +import { IPlatformService } from '../platformService'; +import { EOL } from 'os'; +import { ToolBase } from './toolBase'; const localize = nls.loadMessageBundle(); -export class AzCliTool implements ITool { +export class AzCliTool extends ToolBase { + constructor(platformService: IPlatformService) { + super(platformService); + } + get name(): string { return 'azcli'; } @@ -23,4 +31,19 @@ export class AzCliTool implements ITool { get displayName(): string { return localize('resourceDeployment.AzCLIDisplayName', 'Azure CLI'); } + + get homePage(): string { + return 'https://docs.microsoft.com/cli/azure/install-azure-cli'; + } + + protected getVersionFromOutput(output: string): SemVer | undefined { + if (output && output.includes('azure-cli')) { + return new SemVer(output.split(EOL)[0].replace('azure-cli', '').replace(/ /g, '').replace('*', '')); + } else { + return undefined; + } + } + protected get versionCommand(): string { + return 'az --version'; + } } diff --git a/extensions/resource-deployment/src/services/tools/azdataTool.ts b/extensions/resource-deployment/src/services/tools/azdataTool.ts index c947f1573a..b9ca7bb9d3 100644 --- a/extensions/resource-deployment/src/services/tools/azdataTool.ts +++ b/extensions/resource-deployment/src/services/tools/azdataTool.ts @@ -2,12 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { ToolType, ITool } from '../../interfaces'; + +import { ToolType } from '../../interfaces'; import * as nls from 'vscode-nls'; +import { SemVer } from 'semver'; +import { EOL } from 'os'; +import { IPlatformService } from '../platformService'; +import { ToolBase } from './toolBase'; + const localize = nls.loadMessageBundle(); -export class AzdataTool implements ITool { +export class AzdataTool extends ToolBase { + constructor(platformService: IPlatformService) { + super(platformService); + } + get name(): string { return 'azdata'; } @@ -23,4 +32,20 @@ export class AzdataTool implements ITool { get displayName(): string { return localize('resourceDeployment.AzdataDisplayName', "azdata"); } -} \ No newline at end of file + + get homePage(): string { + return 'https://docs.microsoft.com/sql/big-data-cluster/deploy-install-azdata'; + } + + protected get versionCommand(): string { + return 'azdata -v'; + } + + protected getVersionFromOutput(output: string): SemVer | undefined { + let version: SemVer | undefined = undefined; + if (output && output.split(EOL).length > 0) { + version = new SemVer(output.split(EOL)[0].replace(/ /g, '')); + } + return version; + } +} diff --git a/extensions/resource-deployment/src/services/tools/dockerTool.ts b/extensions/resource-deployment/src/services/tools/dockerTool.ts index d87231d6cd..db7a52c77e 100644 --- a/extensions/resource-deployment/src/services/tools/dockerTool.ts +++ b/extensions/resource-deployment/src/services/tools/dockerTool.ts @@ -2,12 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { ToolType, ITool } from '../../interfaces'; + +import { ToolType } from '../../interfaces'; import * as nls from 'vscode-nls'; +import { SemVer } from 'semver'; +import { IPlatformService } from '../platformService'; +import { ToolBase } from './toolBase'; + const localize = nls.loadMessageBundle(); -export class DockerTool implements ITool { +export class DockerTool extends ToolBase { + constructor(platformService: IPlatformService) { + super(platformService); + } + get name(): string { return 'docker'; } @@ -23,4 +31,19 @@ export class DockerTool implements ITool { get displayName(): string { return localize('resourceDeployment.DockerDisplayName', 'Docker'); } -} \ No newline at end of file + + get homePage(): string { + return 'https://docs.docker.com/install'; + } + + protected getVersionFromOutput(output: string): SemVer | undefined { + let version: SemVer | undefined = undefined; + if (output) { + version = new SemVer(JSON.parse(output).Client.Version, true); + } + return version; + } + protected get versionCommand(): string { + return 'docker version --format "{{json .}}"'; + } +} diff --git a/extensions/resource-deployment/src/services/tools/kubeCtlTool.ts b/extensions/resource-deployment/src/services/tools/kubeCtlTool.ts index c47d154d22..596821b54b 100644 --- a/extensions/resource-deployment/src/services/tools/kubeCtlTool.ts +++ b/extensions/resource-deployment/src/services/tools/kubeCtlTool.ts @@ -2,12 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; -import { ToolType, ITool } from '../../interfaces'; + +import { ToolType } from '../../interfaces'; import * as nls from 'vscode-nls'; +import { SemVer } from 'semver'; +import { IPlatformService } from '../platformService'; +import { ToolBase } from './toolBase'; + const localize = nls.loadMessageBundle(); -export class KubeCtlTool implements ITool { +export class KubeCtlTool extends ToolBase { + constructor(platformService: IPlatformService) { + super(platformService); + } + get name(): string { return 'kubectl'; } @@ -23,4 +31,21 @@ export class KubeCtlTool implements ITool { get displayName(): string { return localize('resourceDeployment.KubeCtlDisplayName', 'kubectl'); } + + get homePage(): string { + return 'https://kubernetes.io/docs/tasks/tools/install-kubectl'; + } + + protected getVersionFromOutput(output: string): SemVer | undefined { + let version: SemVer | undefined = undefined; + if (output) { + const versionJson = JSON.parse(output); + version = new SemVer(`${versionJson.clientVersion.major}.${versionJson.clientVersion.minor}.0`); + } + return version; + } + + protected get versionCommand(): string { + return 'kubectl version -o json --client'; + } } diff --git a/extensions/resource-deployment/src/services/tools/toolBase.ts b/extensions/resource-deployment/src/services/tools/toolBase.ts new file mode 100644 index 0000000000..ff874d3d0a --- /dev/null +++ b/extensions/resource-deployment/src/services/tools/toolBase.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ToolType, ITool } from '../../interfaces'; +import { SemVer } from 'semver'; +import { IPlatformService } from '../platformService'; +import * as nls from 'vscode-nls'; +import { EOL } from 'os'; +const localize = nls.loadMessageBundle(); + +export abstract class ToolBase implements ITool { + constructor(private _platformService: IPlatformService) { } + + abstract name: string; + abstract displayName: string; + abstract description: string; + abstract type: ToolType; + abstract homePage: string; + protected abstract getVersionFromOutput(output: string): SemVer | undefined; + protected abstract readonly versionCommand: string; + + public get version(): SemVer | undefined { + return this._version; + } + + public get isInstalled(): boolean { + return this._isInstalled; + } + + public get statusDescription(): string | undefined { + return this._statusDescription; + } + + public loadInformation(): Promise { + if (this._isInstalled) { + return Promise.resolve(); + } + this._isInstalled = false; + this._statusDescription = undefined; + this._version = undefined; + this._versionOutput = undefined; + return this._platformService.runCommand(this.versionCommand).then((stdout) => { + this._versionOutput = stdout; + this._version = this.getVersionFromOutput(stdout); + if (this._version) { + this._isInstalled = true; + } else { + throw localize('deployCluster.InvalidToolVersionOutput', "Invalid output received."); + } + }).catch((error) => { + const errorMessage = typeof error === 'string' ? error : + typeof error.message === 'string' ? error.message : ''; + this._statusDescription = localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Error: {1}{0}stdout: {2} ", EOL, errorMessage, this._versionOutput); + }); + } + + private _isInstalled: boolean = false; + private _version?: SemVer; + private _statusDescription?: string; + private _versionOutput?: string; +} diff --git a/extensions/resource-deployment/src/services/toolsService.ts b/extensions/resource-deployment/src/services/toolsService.ts index df8be4c9f5..9685a01a8e 100644 --- a/extensions/resource-deployment/src/services/toolsService.ts +++ b/extensions/resource-deployment/src/services/toolsService.ts @@ -2,12 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import { ITool } from '../interfaces'; import { DockerTool } from './tools/dockerTool'; import { AzCliTool } from './tools/azCliTool'; import { AzdataTool } from './tools/azdataTool'; import { KubeCtlTool } from './tools/kubeCtlTool'; +import { IPlatformService } from './platformService'; export interface IToolsService { getToolByName(toolName: string): ITool | undefined; @@ -16,11 +16,11 @@ export interface IToolsService { export class ToolsService implements IToolsService { private supportedTools: ITool[]; - constructor() { - this.supportedTools = [new DockerTool(), new AzCliTool(), new AzdataTool(), new KubeCtlTool()]; + constructor(private _platformService: IPlatformService) { + this.supportedTools = [new DockerTool(this._platformService), new AzCliTool(this._platformService), new AzdataTool(this._platformService), new KubeCtlTool(this._platformService)]; } getToolByName(toolName: string): ITool | undefined { return this.supportedTools.find(t => t.name === toolName); } -} \ No newline at end of file +} diff --git a/extensions/resource-deployment/src/test/notebookService.test.ts b/extensions/resource-deployment/src/test/notebookService.test.ts index 8ab5b85ab0..9ab0e560a7 100644 --- a/extensions/resource-deployment/src/test/notebookService.test.ts +++ b/extensions/resource-deployment/src/test/notebookService.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as TypeMoq from 'typemoq'; import 'mocha'; import { NotebookService } from '../services/notebookService'; @@ -96,4 +94,4 @@ suite('Notebook Service Tests', function (): void { mockPlatformService.verify((service) => service.isNotebookNameUsed(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3)); assert.equal(actualFileName, expectedFileName, 'target file name is not correct'); }); -}); \ No newline at end of file +}); diff --git a/extensions/resource-deployment/src/test/resourceTypeService.test.ts b/extensions/resource-deployment/src/test/resourceTypeService.test.ts index eb31c67bd8..b2396599cb 100644 --- a/extensions/resource-deployment/src/test/resourceTypeService.test.ts +++ b/extensions/resource-deployment/src/test/resourceTypeService.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import 'mocha'; import * as TypeMoq from 'typemoq'; import assert = require('assert'); @@ -18,7 +16,7 @@ suite('Resource Type Service Tests', function (): void { test('test resource types', () => { const mockPlatformService = TypeMoq.Mock.ofType(); - const toolsService = new ToolsService(); + const toolsService = new ToolsService(mockPlatformService.object); const notebookService = new NotebookService(mockPlatformService.object, ''); const resourceTypeService = new ResourceTypeService(mockPlatformService.object, toolsService, notebookService); // index 0: platform name, index 1: number of expected resource types diff --git a/extensions/resource-deployment/src/test/toolsService.test.ts b/extensions/resource-deployment/src/test/toolsService.test.ts index f487b4913f..5715bc43d6 100644 --- a/extensions/resource-deployment/src/test/toolsService.test.ts +++ b/extensions/resource-deployment/src/test/toolsService.test.ts @@ -3,18 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import 'mocha'; import assert = require('assert'); +import * as TypeMoq from 'typemoq'; import { ToolsService } from '../services/toolsService'; import { ToolType } from '../interfaces'; import { isNumber } from 'util'; +import { IPlatformService } from '../services/platformService'; suite('Tools Service Tests', function (): void { test('run getToolByName with all known values', () => { - const toolsService = new ToolsService(); + const mockPlatformService = TypeMoq.Mock.ofType(); + const toolsService = new ToolsService(mockPlatformService.object); const tools: { name: string; type: ToolType }[] = [ { name: 'azcli', type: ToolType.AzCli }, @@ -42,8 +43,9 @@ suite('Tools Service Tests', function (): void { }); test('run getToolByName with a name that is not defined', () => { - const toolsService = new ToolsService(); + const mockPlatformService = TypeMoq.Mock.ofType(); + const toolsService = new ToolsService(mockPlatformService.object); const tool = toolsService.getToolByName('no-such-tool'); assert(tool === undefined, 'for a not defined tool, expected value is undefined'); }); -}); \ No newline at end of file +}); diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts new file mode 100644 index 0000000000..079abce4b9 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const DeploymentProfile_VariableName = 'AZDATA_NB_VAR_BDC_DEPLOYMENT_PROFILE'; +export const ClusterName_VariableName = 'AZDATA_NB_VAR_BDC_CLUSTER_NAME'; +export const AdminUserName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME'; +export const AdminPassword_VariableName = 'AZDATA_NB_VAR_BDC_ADMIN_PASSWORD'; +export const AuthenticationMode_VariableName = 'AZDATA_NB_VAR_BDC_AUTHENTICATION_MODE'; +export const DistinguishedName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DN'; +export const AdminPrincipals_VariableName = 'AZDATA_NB_VAR_BDC_AD_ADMIN_PRINCIPALS'; +export const UserPrincipals_VariableName = 'AZDATA_NB_VAR_BDC_AD_USER_PRINCIPALS'; +export const UpstreamIPAddresses_VariableName = 'AZDATA_NB_VAR_BDC_AD_UPSTREAM_IPADDRESSES'; +export const DnsName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DNS_NAME'; +export const Realm_VariableName = 'AZDATA_NB_VAR_BDC_AD_REALM'; +export const AppOwnerPrincipals_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_OWNER_PRINCIPALS'; +export const AppReaderPrincipals_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_READER_PRINCIPALS'; +export const SubscriptionId_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_SUBSCRIPTION'; +export const ResourceGroup_VariableName = 'AZDATA_NB_VAR_BDC_RESOURCEGROUP_NAME'; +export const Region_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_REGION'; +export const AksName_VariableName = 'AZDATA_NB_VAR_BDC_AKS_NAME'; +export const VMSize_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_VM_SIZE'; +export const VMCount_VariableName = 'AZDATA_NB_VAR_BDC_VM_COUNT'; +export const KubeConfigPath_VariableName = 'AZDATA_NB_VAR_BDC_KUBECONFIG_PATH'; +export const ClusterContext_VariableName = 'AZDATA_NB_VAR_BDC_CLUSTER_CONTEXT'; +export const SQLServerScale_VariableName = 'AZDATA_NB_VAR_BDC_SQLSERVER_SCALE'; +export const HDFSPoolScale_VariableName = 'AZDATA_NB_VAR_BDC_HDFSPOOL_SCALE'; +export const HDFSNameNodeScale_VariableName = 'AZDATA_NB_VAR_BDC_NAMENODE_SCALE'; +export const ZooKeeperScale_VariableName = 'AZDATA_NB_VAR_BDC_ZOOKEEPER_SCALE'; +export const SparkHeadScale_VariableName = 'AZDATA_NB_VAR_BDC_SPARKHEAD_SCALE'; +export const IncludeSpark_VariableName = 'AZDATA_NB_VAR_BDC_INCLUDESPARK'; +export const ComputePoolScale_VariableName = 'AZDATA_NB_VAR_BDC_COMPUTEPOOL_SCALE'; +export const DataPoolScale_VariableName = 'AZDATA_NB_VAR_BDC_DATAPOOL_SCALE'; +export const SparkPoolScale_VariableName = 'AZDATA_NB_VAR_BDC_SPARKPOOL_SCALE'; +export const ControllerDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_DATA_STORAGE_CLASS'; +export const ControllerDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_DATA_STORAGE_SIZE'; +export const ControllerLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_LOGS_STORAGE_CLASS'; +export const ControllerLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_LOGS_STORAGE_SIZE'; +export const DataPoolDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_DATA_DATA_STORAGE_CLASS'; +export const DataPoolDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_DATA_DATA_STORAGE_SIZE'; +export const DataPoolLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_DATA_LOGS_STORAGE_CLASS'; +export const DataPoolLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_DATA_LOGS_STORAGE_SIZE'; +export const HDFSDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_DATA_STORAGE_CLASS'; +export const HDFSDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_DATA_STORAGE_SIZE'; +export const HDFSLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_LOGS_STORAGE_CLASS'; +export const HDFSLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_LOGS_STORAGE_SIZE'; +export const SQLServerDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_SQL_DATA_STORAGE_CLASS'; +export const SQLServerDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_SQL_DATA_STORAGE_SIZE'; +export const SQLServerLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_SQL_LOGS_STORAGE_CLASS'; +export const SQLServerLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_SQL_LOGS_STORAGE_SIZE'; +export const ControllerDNSName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_DNS'; +export const ControllerPort_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_PORT'; +export const SQLServerDNSName_VariableName = 'AZDATA_NB_VAR_BDC_SQL_DNS'; +export const SQLServerPort_VariableName = 'AZDATA_NB_VAR_BDC_SQL_PORT'; +export const GatewayDNSName_VariableName = 'AZDATA_NB_VAR_BDC_GATEWAY_DNS'; +export const GateWayPort_VariableName = 'AZDATA_NB_VAR_BDC_GATEWAY_PORT'; +export const ReadableSecondaryDNSName_VariableName = 'AZDATA_NB_VAR_BDC_READABLE_SECONDARY_DNS'; +export const ReadableSecondaryPort_VariableName = 'AZDATA_NB_VAR_BDC_READABLE_SECONDARY_PORT'; +export const EnableHADR_VariableName = 'AZDATA_NB_VAR_BDC_ENABLE_HADR'; diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts new file mode 100644 index 0000000000..aca03654e5 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SummaryPage } from './pages/summaryPage'; +import { WizardBase } from '../wizardBase'; +import * as nls from 'vscode-nls'; +import { WizardInfo, BdcDeploymentType } from '../../interfaces'; +import { WizardPageBase } from '../wizardPageBase'; +import { AzureSettingsPage } from './pages/azureSettingsPage'; +import { ClusterSettingsPage } from './pages/clusterSettingsPage'; +import { ServiceSettingsPage } from './pages/serviceSettingsPage'; +import { TargetClusterContextPage } from './pages/targetClusterPage'; +import { IKubeService } from '../../services/kubeService'; +import { IAzdataService } from '../../services/azdataService'; +import { DeploymentProfilePage } from './pages/deploymentProfilePage'; +import { INotebookService } from '../../services/notebookService'; +import { DeployClusterWizardModel } from './deployClusterWizardModel'; +import * as VariableNames from './constants'; +const localize = nls.loadMessageBundle(); + +export class DeployClusterWizard extends WizardBase { + + public get kubeService(): IKubeService { + return this._kubeService; + } + + public get azdataService(): IAzdataService { + return this._azdataService; + } + + public get notebookService(): INotebookService { + return this._notebookService; + } + + constructor(private wizardInfo: WizardInfo, private _kubeService: IKubeService, private _azdataService: IAzdataService, private _notebookService: INotebookService) { + super(DeployClusterWizard.getTitle(wizardInfo.type), new DeployClusterWizardModel(wizardInfo.type)); + } + + public get deploymentType(): BdcDeploymentType { + return this.wizardInfo.type; + } + + protected initialize(): void { + this.setPages(this.getPages()); + this.wizardObject.generateScriptButton.hidden = true; + this.wizardObject.doneButton.label = localize('deployCluster.openNotebook', 'Open Notebook'); + } + + protected onCancel(): void { + } + + protected onOk(): void { + process.env[VariableNames.AdminPassword_VariableName] = this.model.getStringValue(VariableNames.AdminPassword_VariableName); + this.notebookService.launchNotebook(this.wizardInfo.notebook).then((notebook: azdata.nb.NotebookEditor) => { + notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { + editBuilder.insertCell({ + cell_type: 'code', + source: this.model.getCodeCellContentForNotebook() + }, 7); + }); + }, (error) => { + vscode.window.showErrorMessage(error); + }); + } + + private getPages(): WizardPageBase[] { + const pages: WizardPageBase[] = []; + switch (this.deploymentType) { + case BdcDeploymentType.NewAKS: + pages.push( + new DeploymentProfilePage(this), + new AzureSettingsPage(this), + new ClusterSettingsPage(this), + new ServiceSettingsPage(this), + new SummaryPage(this)); + break; + case BdcDeploymentType.ExistingAKS: + pages.push( + new DeploymentProfilePage(this), + new TargetClusterContextPage(this), + new ClusterSettingsPage(this), + new ServiceSettingsPage(this), + new SummaryPage(this)); + break; + case BdcDeploymentType.ExistingKubeAdm: + pages.push( + new DeploymentProfilePage(this), + new TargetClusterContextPage(this), + new ClusterSettingsPage(this), + new ServiceSettingsPage(this), + new SummaryPage(this)); + break; + default: + throw new Error(`Unknown deployment type: ${this.deploymentType}`); + } + return pages; + } + + static getTitle(type: BdcDeploymentType): string { + switch (type) { + case BdcDeploymentType.NewAKS: + return localize('deployCluster.NewAKSWizardTitle', "Deploy SQL Server 2019 Big Data Cluster on a new AKS cluster"); + case BdcDeploymentType.ExistingAKS: + return localize('deployCluster.ExistingAKSWizardTitle', "Deploy SQL Server 2019 Big Data Cluster on an existing AKS cluster"); + case BdcDeploymentType.ExistingKubeAdm: + return localize('deployCluster.ExistingKubeAdm', "Deploy SQL Server 2019 Big Data Cluster on an existing kubeadm cluster"); + default: + throw new Error(`Unknown deployment type: ${type}`); + } + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts new file mode 100644 index 0000000000..ccbb9f112f --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Model } from '../model'; +import * as VariableNames from './constants'; +import { BigDataClusterDeploymentProfile, DataResource, SqlServerMasterResource, HdfsResource } from '../../services/bigDataClusterDeploymentProfile'; +import { BdcDeploymentType } from '../../interfaces'; +import { EOL } from 'os'; + +export class DeployClusterWizardModel extends Model { + constructor(public deploymentTarget: BdcDeploymentType) { + super(); + } + public adAuthSupported: boolean = false; + + public get hadrEnabled(): boolean { + return this.getBooleanValue(VariableNames.EnableHADR_VariableName); + } + + public set hadrEnabled(value: boolean) { + this.setPropertyValue(VariableNames.EnableHADR_VariableName, value); + } + + public get authenticationMode(): string | undefined { + return this.getStringValue(VariableNames.AuthenticationMode_VariableName); + } + + public set authenticationMode(value: string | undefined) { + this.setPropertyValue(VariableNames.AuthenticationMode_VariableName, value); + } + + public getStorageSettingValue(propertyName: string, defaultValuePropertyName: string): string | undefined { + const value = this.getStringValue(propertyName); + return (value === undefined || value === '') ? this.getStringValue(defaultValuePropertyName) : value; + } + + private setStorageSettingValue(propertyName: string, defaultValuePropertyName: string): void { + const value = this.getStringValue(propertyName); + if (value === undefined || value === '') { + this.setPropertyValue(propertyName, this.getStringValue(defaultValuePropertyName)); + } + } + + private setStorageSettingValues(): void { + this.setStorageSettingValue(VariableNames.DataPoolDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName); + this.setStorageSettingValue(VariableNames.DataPoolDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName); + this.setStorageSettingValue(VariableNames.DataPoolLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName); + this.setStorageSettingValue(VariableNames.DataPoolLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName); + + this.setStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName); + this.setStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName); + this.setStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName); + this.setStorageSettingValue(VariableNames.HDFSLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName); + + this.setStorageSettingValue(VariableNames.SQLServerDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName); + this.setStorageSettingValue(VariableNames.SQLServerDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName); + this.setStorageSettingValue(VariableNames.SQLServerLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName); + this.setStorageSettingValue(VariableNames.SQLServerLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName); + } + + public setEnvironmentVariables(): void { + this.setStorageSettingValues(); + } + + public selectedProfile: BigDataClusterDeploymentProfile | undefined; + + public createTargetProfile(): BigDataClusterDeploymentProfile { + // create a copy of the source files to avoid changing the source profile values + const sourceBdcJson = Object.assign({}, this.selectedProfile!.bdcConfig); + const sourceControlJson = Object.assign({}, this.selectedProfile!.controlConfig); + const targetDeploymentProfile = new BigDataClusterDeploymentProfile('', sourceBdcJson, sourceControlJson); + // cluster name + targetDeploymentProfile.clusterName = this.getStringValue(VariableNames.ClusterName_VariableName)!; + // storage settings + targetDeploymentProfile.controllerDataStorageClass = this.getStringValue(VariableNames.ControllerDataStorageClassName_VariableName)!; + targetDeploymentProfile.controllerDataStorageSize = this.getIntegerValue(VariableNames.ControllerDataStorageSize_VariableName)!; + targetDeploymentProfile.controllerLogsStorageClass = this.getStringValue(VariableNames.ControllerLogsStorageClassName_VariableName)!; + targetDeploymentProfile.controllerLogsStorageSize = this.getIntegerValue(VariableNames.ControllerLogsStorageSize_VariableName)!; + targetDeploymentProfile.setResourceStorage(DataResource, + this.getStorageSettingValue(VariableNames.DataPoolDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName)!, + Number.parseInt(this.getStorageSettingValue(VariableNames.DataPoolDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName)!), + this.getStorageSettingValue(VariableNames.DataPoolLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName)!, + Number.parseInt(this.getStorageSettingValue(VariableNames.DataPoolLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)!) + ); + targetDeploymentProfile.setResourceStorage(SqlServerMasterResource, + this.getStorageSettingValue(VariableNames.SQLServerDNSName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName)!, + Number.parseInt(this.getStorageSettingValue(VariableNames.SQLServerDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName)!), + this.getStorageSettingValue(VariableNames.SQLServerLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName)!, + Number.parseInt(this.getStorageSettingValue(VariableNames.SQLServerLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)!) + ); + targetDeploymentProfile.setResourceStorage(HdfsResource, + this.getStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName)!, + Number.parseInt(this.getStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName)!), + this.getStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName)!, + Number.parseInt(this.getStorageSettingValue(VariableNames.HDFSLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)!) + ); + + // scale settings + targetDeploymentProfile.dataReplicas = this.getIntegerValue(VariableNames.DataPoolScale_VariableName); + targetDeploymentProfile.computeReplicas = this.getIntegerValue(VariableNames.ComputePoolScale_VariableName); + targetDeploymentProfile.hdfsReplicas = this.getIntegerValue(VariableNames.HDFSPoolScale_VariableName); + targetDeploymentProfile.sqlServerReplicas = this.getIntegerValue(VariableNames.SQLServerScale_VariableName); + targetDeploymentProfile.hdfsNameNodeReplicas = this.getIntegerValue(VariableNames.HDFSNameNodeScale_VariableName); + targetDeploymentProfile.sparkHeadReplicas = this.getIntegerValue(VariableNames.SparkHeadScale_VariableName); + targetDeploymentProfile.zooKeeperReplicas = this.getIntegerValue(VariableNames.ZooKeeperScale_VariableName); + const sparkScale = this.getIntegerValue(VariableNames.SparkPoolScale_VariableName); + if (sparkScale > 0) { + targetDeploymentProfile.addSparkResource(sparkScale); + } + + targetDeploymentProfile.includeSpark = this.getBooleanValue(VariableNames.IncludeSpark_VariableName); + targetDeploymentProfile.hadrEnabled = this.getBooleanValue(VariableNames.EnableHADR_VariableName); + + // port settings + targetDeploymentProfile.gatewayPort = this.getIntegerValue(VariableNames.GateWayPort_VariableName); + targetDeploymentProfile.sqlServerPort = this.getIntegerValue(VariableNames.SQLServerPort_VariableName); + targetDeploymentProfile.controllerPort = this.getIntegerValue(VariableNames.ControllerPort_VariableName); + targetDeploymentProfile.sqlServerReadableSecondaryPort = this.getIntegerValue(VariableNames.ReadableSecondaryPort_VariableName); + + return targetDeploymentProfile; + } + + public getCodeCellContentForNotebook(): string { + const profile = this.createTargetProfile(); + const statements: string[] = []; + if (this.deploymentTarget === BdcDeploymentType.NewAKS) { + statements.push(`azure_subscription_id = '${this.getStringValue(VariableNames.SubscriptionId_VariableName, '')}'`); + statements.push(`azure_region = '${this.getStringValue(VariableNames.Region_VariableName)}'`); + statements.push(`azure_resource_group = '${this.getStringValue(VariableNames.ResourceGroup_VariableName)}'`); + statements.push(`azure_vm_size = '${this.getStringValue(VariableNames.VMSize_VariableName)}'`); + statements.push(`azure_vm_count = '${this.getStringValue(VariableNames.VMCount_VariableName)}'`); + statements.push(`aks_cluster_name = '${this.getStringValue(VariableNames.AksName_VariableName)}'`); + } else if (this.deploymentTarget === BdcDeploymentType.ExistingAKS || this.deploymentTarget === BdcDeploymentType.ExistingKubeAdm) { + statements.push(`mssql_kube_config_path = '${this.getStringValue(VariableNames.KubeConfigPath_VariableName)}'`); + statements.push(`mssql_cluster_context = '${this.getStringValue(VariableNames.ClusterContext_VariableName)}'`); + statements.push('os.environ["KUBECONFIG"] = mssql_kube_config_path'); + } + statements.push(`mssql_cluster_name = '${this.getStringValue(VariableNames.ClusterName_VariableName)}'`); + statements.push(`mssql_controller_username = '${this.getStringValue(VariableNames.AdminUserName_VariableName)}'`); + statements.push(`bdc_json = '${profile.getBdcJson(false)}'`); + statements.push(`control_json = '${profile.getControlJson(false)}'`); + statements.push(`print('Variables have been set successfully.')`); + return statements.join(EOL); + } +} + +export enum AuthenticationMode { + ActiveDirectory = 'ad', + Basic = 'basic' +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts new file mode 100644 index 0000000000..c8256b8997 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import { DeployClusterWizard } from '../deployClusterWizard'; +import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces'; +import { WizardPageBase } from '../../wizardPageBase'; +import { createSection, InputComponents, setModelValues, Validator } from '../../modelViewUtils'; +import { SubscriptionId_VariableName, ResourceGroup_VariableName, Region_VariableName, AksName_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants'; +const localize = nls.loadMessageBundle(); + +export class AzureSettingsPage extends WizardPageBase { + private inputComponents: InputComponents = {}; + + constructor(wizard: DeployClusterWizard) { + super(localize('deployCluster.AzureSettingsPageTitle', "Azure settings"), + localize('deployCluster.AzureSettingsPageDescription', "Configure the settings to create an Azure Kubernetes Service cluster"), wizard); + } + + public initialize(): void { + const self = this; + const azureSection: SectionInfo = { + title: '', + labelPosition: LabelPosition.Left, + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.SubscriptionField', "Subscription id"), + required: false, + variableName: SubscriptionId_VariableName, + placeHolder: localize('deployCluster.SubscriptionPlaceholder', "Use my default Azure subscription"), + description: localize('deployCluster.SubscriptionDescription', "The default subscription will be used if you leave this field blank.") + }, { + type: FieldType.DateTimeText, + label: localize('deployCluster.ResourceGroupName', "New resource group name"), + required: true, + variableName: ResourceGroup_VariableName, + defaultValue: 'mssql-' + }, { + type: FieldType.Text, + label: localize('deployCluster.Region', "Region"), + required: true, + variableName: Region_VariableName, + defaultValue: 'eastus' + }, { + type: FieldType.DateTimeText, + label: localize('deployCluster.AksName', "AKS cluster name"), + required: true, + variableName: AksName_VariableName, + defaultValue: 'mssql-', + }, { + type: FieldType.Number, + label: localize('deployCluster.VMCount', "VM count"), + required: true, + variableName: VMCount_VariableName, + defaultValue: '5', + min: 1, + max: 999 + }, { + type: FieldType.Text, + label: localize('deployCluster.VMSize', "VM size"), + required: true, + variableName: VMSize_VariableName, + defaultValue: 'Standard_E4s_v3' + } + ] + }; + this.pageObject.registerContent((view: azdata.ModelView) => { + + const azureGroup = createSection({ + sectionInfo: azureSection, + view: view, + onNewDisposableCreated: (disposable: vscode.Disposable): void => { + self.wizard.registerDisposable(disposable); + }, + onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent): void => { + self.inputComponents[name] = component; + }, + onNewValidatorCreated: (validator: Validator): void => { + self.validators.push(validator); + }, + container: this.wizard.wizardObject + }); + const formBuilder = view.modelBuilder.formContainer().withFormItems( + [{ + title: '', + component: azureGroup + }], + { + horizontal: false, + componentWidth: '100%' + } + ); + + const form = formBuilder.withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); + } + + public onLeave(): void { + setModelValues(this.inputComponents, this.wizard.model); + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts new file mode 100644 index 0000000000..6e874db672 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts @@ -0,0 +1,251 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import { DeployClusterWizard } from '../deployClusterWizard'; +import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces'; +import { createSection, InputComponents, setModelValues, Validator, isInputBoxEmpty, getInputBoxComponent, isValidSQLPassword, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, MissingRequiredInformationErrorMessage } from '../../modelViewUtils'; +import { WizardPageBase } from '../../wizardPageBase'; +import * as VariableNames from '../constants'; +import { EOL } from 'os'; +import { AuthenticationMode } from '../deployClusterWizardModel'; +const localize = nls.loadMessageBundle(); + +const ConfirmPasswordName = 'ConfirmPassword'; +export class ClusterSettingsPage extends WizardPageBase { + private inputComponents: InputComponents = {}; + + constructor(wizard: DeployClusterWizard) { + super(localize('deployCluster.ClusterSettingsPageTitle', "Cluster settings"), + localize('deployCluster.ClusterSettingsPageDescription', "Configure the SQL Server Big Data Cluster settings"), wizard); + } + + public initialize(): void { + const self = this; + const basicSection: SectionInfo = { + labelPosition: LabelPosition.Left, + title: '', + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.ClusterName', "Cluster name"), + required: true, + variableName: VariableNames.ClusterName_VariableName, + defaultValue: 'mssql-cluster', + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.ControllerUsername', "Controller username"), + required: true, + variableName: VariableNames.AdminUserName_VariableName, + defaultValue: 'admin', + useCustomValidator: true + }, { + type: FieldType.Password, + label: localize('deployCluster.AdminPassword', "Password"), + required: true, + variableName: VariableNames.AdminPassword_VariableName, + defaultValue: '', + useCustomValidator: true, + description: localize('deployCluster.AdminPasswordDescription', "You can also use this password to access SQL Server and gateway.") + }, { + type: FieldType.Password, + label: localize('deployCluster.ConfirmPassword', "Confirm password"), + required: true, + variableName: ConfirmPasswordName, + defaultValue: '', + useCustomValidator: true, + }, { + type: FieldType.Options, + label: localize('deployCluster.AuthenticationMode', "Authentication mode"), + required: true, + variableName: VariableNames.AuthenticationMode_VariableName, + defaultValue: AuthenticationMode.Basic, + options: [ + { + name: AuthenticationMode.Basic, + displayName: localize('deployCluster.AuthenticationMode.Basic', "Basic") + }, + { + name: AuthenticationMode.ActiveDirectory, + displayName: localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") + + } + ] + } + ] + }; + + const activeDirectorySection: SectionInfo = { + labelPosition: LabelPosition.Left, + title: localize('deployCluster.ActiveDirectorySettings', "Active Directory settings"), + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.DistinguishedName', "Distinguished name"), + required: true, + variableName: VariableNames.DistinguishedName_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.AdminPrincipals', "Admin principals"), + required: true, + variableName: VariableNames.AdminPrincipals_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.UserPrincipals', "User principals"), + required: true, + variableName: VariableNames.UserPrincipals_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.UpstreamIPAddresses', "Upstream IP Addresses"), + required: true, + variableName: VariableNames.UpstreamIPAddresses_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.DNSName', "DNS name"), + required: true, + variableName: VariableNames.DnsName_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.Realm', "Realm"), + required: true, + variableName: VariableNames.Realm_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.AppOnwerPrincipals', "App owner principals"), + required: true, + variableName: VariableNames.AppOwnerPrincipals_VariableName, + useCustomValidator: true + }, { + type: FieldType.Text, + label: localize('deployCluster.AppReaderPrincipals', "App reader principals"), + required: true, + variableName: VariableNames.AppReaderPrincipals_VariableName, + useCustomValidator: true + } + ] + }; + this.pageObject.registerContent((view: azdata.ModelView) => { + const basicSettingsGroup = createSection({ + view: view, + container: self.wizard.wizardObject, + sectionInfo: basicSection, + onNewDisposableCreated: (disposable: vscode.Disposable): void => { + self.wizard.registerDisposable(disposable); + }, + onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { + self.inputComponents[name] = component; + }, + onNewValidatorCreated: (validator: Validator): void => { + self.validators.push(validator); + } + }); + const activeDirectorySettingsGroup = createSection({ + view: view, + container: self.wizard.wizardObject, + sectionInfo: activeDirectorySection, + onNewDisposableCreated: (disposable: vscode.Disposable): void => { + self.wizard.registerDisposable(disposable); + }, + onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { + self.inputComponents[name] = component; + }, + onNewValidatorCreated: (validator: Validator): void => { + self.validators.push(validator); + } + }); + const basicSettingsFormItem = { title: '', component: basicSettingsGroup }; + const activeDirectoryFormItem = { title: '', component: activeDirectorySettingsGroup }; + const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName]; + const formBuilder = view.modelBuilder.formContainer().withFormItems( + [basicSettingsFormItem], + { + horizontal: false, + componentWidth: '100%' + } + ); + this.wizard.registerDisposable(authModeDropdown.onValueChanged(() => { + const isBasicAuthMode = (authModeDropdown.value).name === 'basic'; + + if (isBasicAuthMode) { + formBuilder.removeFormItem(activeDirectoryFormItem); + } else { + formBuilder.insertFormItem(activeDirectoryFormItem); + } + })); + + const form = formBuilder.withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); + } + + public onLeave() { + setModelValues(this.inputComponents, this.wizard.model); + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + public onEnter() { + const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName]; + if (authModeDropdown) { + authModeDropdown.enabled = this.wizard.model.adAuthSupported; + } + + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + this.wizard.wizardObject.message = { text: '' }; + if (pcInfo.newPage > pcInfo.lastPage) { + const messages: string[] = []; + const authMode = typeof authModeDropdown.value === 'string' ? authModeDropdown.value : authModeDropdown.value!.name; + const requiredFieldsFilled: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ClusterName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminUserName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents)) + && (!(authMode === AuthenticationMode.ActiveDirectory) || ( + !isInputBoxEmpty(getInputBoxComponent(VariableNames.DistinguishedName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPrincipals_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.UserPrincipals_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.UpstreamIPAddresses_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DnsName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.Realm_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppOwnerPrincipals_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppReaderPrincipals_VariableName, this.inputComponents)))); + if (!requiredFieldsFilled) { + messages.push(MissingRequiredInformationErrorMessage); + } + + if (!isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents))) { + const password = getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents).value!; + const confirmPassword = getInputBoxComponent(ConfirmPasswordName, this.inputComponents).value!; + if (password !== confirmPassword) { + messages.push(getPasswordMismatchMessage(localize('deployCluster.AdminPasswordField', "Password"))); + } + if (!isValidSQLPassword(password)) { + messages.push(getInvalidSQLPasswordMessage(localize('deployCluster.AdminPasswordField', "Password"))); + } + } + + if (messages.length > 0) { + this.wizard.wizardObject.message = { + text: messages.length === 1 ? messages[0] : localize('deployCluster.ValidationError', "There are some errors on this page, click 'Show Details' to view the errors."), + description: messages.length === 1 ? undefined : messages.join(EOL), + level: azdata.window.MessageLevel.Error + }; + } + return messages.length === 0; + } + return true; + }); + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts new file mode 100644 index 0000000000..37c74771c0 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +import { DeployClusterWizard } from '../deployClusterWizard'; +import { WizardPageBase } from '../../wizardPageBase'; +import * as VariableNames from '../constants'; +import { createFlexContainer } from '../../modelViewUtils'; +import { BdcDeploymentType } from '../../../interfaces'; +import { BigDataClusterDeploymentProfile } from '../../../services/bigDataClusterDeploymentProfile'; +const localize = nls.loadMessageBundle(); + +export class DeploymentProfilePage extends WizardPageBase { + + private _cards: azdata.CardComponent[] = []; + private _cardContainer: azdata.FlexContainer | undefined; + private _loadingComponent: azdata.LoadingComponent | undefined; + private _view: azdata.ModelView | undefined; + + constructor(wizard: DeployClusterWizard) { + super(localize('deployCluster.summaryPageTitle', "Deployment configuration template"), + localize('deployCluster.summaryPageDescription', "Select the target configuration template"), wizard); + } + + public initialize(): void { + this.pageObject.registerContent((view: azdata.ModelView) => { + this._view = view; + this._cardContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', flexWrap: 'wrap' }).component(); + const hintText = view.modelBuilder.text().withProperties({ + value: localize('deployCluster.ProfileHintText', "Note: The settings of the deployment profile can be customized in later steps.") + }).component(); + const container = createFlexContainer(view, [this._cardContainer, hintText], false); + this._loadingComponent = view.modelBuilder.loadingComponent().withItem(container).withProperties({ + loading: true + }).component(); + let formBuilder = view.modelBuilder.formContainer().withFormItems( + [ + { + title: '', + component: this._loadingComponent + } + ], + { + horizontal: false + } + ).withLayout({ width: '100%', height: '100%' }); + const form = formBuilder.withLayout({ width: '100%' }).component(); + this.loadCards().then(() => { + this._loadingComponent!.loading = false; + }, (error) => { + this.wizard.wizardObject.message = { + level: azdata.window.MessageLevel.Error, + text: localize('deployCluster.loadProfileFailed', "Failed to load the deployment profiles: {0}", error.message) + }; + this._loadingComponent!.loading = false; + }); + return view.initializeModel(form); + }); + } + + private createProfileCard(profile: BigDataClusterDeploymentProfile, view: azdata.ModelView): azdata.CardComponent { + const descriptions: azdata.CardDescriptionItem[] = [{ + label: localize('deployCluster.serviceLabel', "Service"), + value: localize('deployCluster.instancesLabel', "Instances"), + fontWeight: 'bold' + }, { + label: localize('deployCluster.masterPoolLabel', "SQL Server Master"), + value: profile.sqlServerReplicas.toString() + }, { + label: localize('deployCluster.computePoolLable', "Compute"), + value: profile.computeReplicas.toString() + }, { + label: localize('deployCluster.dataPoolLabel', "Data"), + value: profile.dataReplicas.toString() + }, { + label: localize('deployCluster.hdfsLabel', "HDFS + Spark"), + value: profile.hdfsReplicas.toString() + }, { + label: '' // line separator + }, { + label: localize('deployCluster.defaultDataStorage', "Data storage size (GB)"), + value: profile.controllerDataStorageSize.toString() + }, { + label: localize('deployCluster.defaultLogStorage', "Log storage size (GB)"), + value: profile.controllerLogsStorageSize.toString() + }, { + label: '' // line separator + } + ]; + if (profile.activeDirectorySupported) { + descriptions.push({ + label: localize('deployCluster.activeDirectoryAuthentication', "Active Directory authentication"), + value: '✅' + }); + } else { + descriptions.push({ + label: localize('deployCluster.basicAuthentication', "Basic authentication"), + value: '✅' + }); + } + + if (profile.hadrEnabled) { + descriptions.push({ + label: localize('deployCluster.hadr', "High Availability"), + value: '✅' + }); + } + + const card = view.modelBuilder.card().withProperties({ + cardType: azdata.CardType.VerticalButton, + label: profile.profileName, + descriptions: descriptions, + width: '240px', + height: '300px', + }).component(); + this._cards.push(card); + this.wizard.registerDisposable(card.onCardSelectedChanged(() => { + if (card.selected) { + this.wizard.wizardObject.message = { text: '' }; + this.setModelValuesByProfile(profile); + // clear the selected state of the previously selected card + this._cards.forEach(c => { + if (c !== card) { + c.selected = false; + } + }); + } else { + // keep the selected state if no other card is selected + if (this._cards.filter(c => { return c !== card && c.selected; }).length === 0) { + card.selected = true; + } + } + })); + + return card; + } + + private setModelValuesByProfile(selectedProfile: BigDataClusterDeploymentProfile): void { + this.wizard.model.setPropertyValue(VariableNames.DeploymentProfile_VariableName, selectedProfile.profileName); + this.wizard.model.setPropertyValue(VariableNames.SparkPoolScale_VariableName, selectedProfile.sparkReplicas); + this.wizard.model.setPropertyValue(VariableNames.DataPoolScale_VariableName, selectedProfile.dataReplicas); + this.wizard.model.setPropertyValue(VariableNames.HDFSPoolScale_VariableName, selectedProfile.hdfsReplicas); + this.wizard.model.setPropertyValue(VariableNames.ComputePoolScale_VariableName, selectedProfile.computeReplicas); + this.wizard.model.setPropertyValue(VariableNames.HDFSNameNodeScale_VariableName, selectedProfile.hdfsNameNodeReplicas); + this.wizard.model.setPropertyValue(VariableNames.SQLServerScale_VariableName, selectedProfile.sqlServerReplicas); + this.wizard.model.setPropertyValue(VariableNames.SparkHeadScale_VariableName, selectedProfile.sparkHeadReplicas); + this.wizard.model.setPropertyValue(VariableNames.ZooKeeperScale_VariableName, selectedProfile.zooKeeperReplicas); + this.wizard.model.setPropertyValue(VariableNames.ControllerDataStorageSize_VariableName, selectedProfile.controllerDataStorageSize); + this.wizard.model.setPropertyValue(VariableNames.ControllerLogsStorageSize_VariableName, selectedProfile.controllerLogsStorageSize); + this.wizard.model.setPropertyValue(VariableNames.EnableHADR_VariableName, selectedProfile.hadrEnabled); + this.wizard.model.setPropertyValue(VariableNames.SQLServerPort_VariableName, selectedProfile.sqlServerPort); + this.wizard.model.setPropertyValue(VariableNames.GateWayPort_VariableName, selectedProfile.gatewayPort); + this.wizard.model.setPropertyValue(VariableNames.ControllerPort_VariableName, selectedProfile.controllerPort); + this.wizard.model.setPropertyValue(VariableNames.IncludeSpark_VariableName, selectedProfile.includeSpark); + this.wizard.model.setPropertyValue(VariableNames.ControllerDataStorageClassName_VariableName, selectedProfile.controllerDataStorageClass); + this.wizard.model.setPropertyValue(VariableNames.ControllerLogsStorageClassName_VariableName, selectedProfile.controllerLogsStorageClass); + this.wizard.model.setPropertyValue(VariableNames.ReadableSecondaryPort_VariableName, selectedProfile.sqlServerReadableSecondaryPort); + this.wizard.model.adAuthSupported = selectedProfile.activeDirectorySupported; + this.wizard.model.selectedProfile = selectedProfile; + } + + private loadCards(): Promise { + return this.wizard.azdataService.getDeploymentProfiles().then((profiles: BigDataClusterDeploymentProfile[]) => { + const defaultProfile: string = this.getDefaultProfile(); + + profiles.forEach(profile => { + const card = this.createProfileCard(profile, this._view!); + if (profile.profileName === defaultProfile) { + card.selected = true; + this.setModelValuesByProfile(profile); + } + this._cardContainer!.addItem(card, { flex: '0 0 auto' }); + }); + }); + } + + public onEnter() { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + this.wizard.wizardObject.message = { text: '' }; + if (pcInfo.newPage > pcInfo.lastPage) { + const isValid = this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName) !== undefined; + if (!isValid) { + this.wizard.wizardObject.message = { + text: localize('deployCluster.ProfileNotSelectedError', "Please select a deployment profile."), + level: azdata.window.MessageLevel.Error + }; + } + return isValid; + } + return true; + }); + } + + public onLeave() { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + private getDefaultProfile(): string { + switch (this.wizard.deploymentType) { + case BdcDeploymentType.NewAKS: + case BdcDeploymentType.ExistingAKS: + return 'aks-dev-test'; + case BdcDeploymentType.ExistingKubeAdm: + return 'kubeadm-dev-test'; + default: + throw new Error(`Unknown deployment type: ${this.wizard.deploymentType}`); + } + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts new file mode 100644 index 0000000000..8f0472ba00 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts @@ -0,0 +1,563 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import { DeployClusterWizard } from '../deployClusterWizard'; +import { SectionInfo, FieldType } from '../../../interfaces'; +import { Validator, InputComponents, createSection, createGroupContainer, createLabel, createFlexContainer, createTextInput, createNumberInput, setModelValues, getInputBoxComponent, getCheckboxComponent, isInputBoxEmpty, getDropdownComponent, MissingRequiredInformationErrorMessage } from '../../modelViewUtils'; +import { WizardPageBase } from '../../wizardPageBase'; +import * as VariableNames from '../constants'; +import { AuthenticationMode } from '../deployClusterWizardModel'; +const localize = nls.loadMessageBundle(); + +const PortInputWidth = '100px'; +const inputWidth = '180px'; +const labelWidth = '150px'; +const spaceBetweenFields = '5px'; + +export class ServiceSettingsPage extends WizardPageBase { + private inputComponents: InputComponents = {}; + private endpointHeaderRow!: azdata.FlexContainer; + private dnsColumnHeader!: azdata.TextComponent; + private portColumnHeader!: azdata.TextComponent; + private controllerDNSInput!: azdata.InputBoxComponent; + private controllerPortInput!: azdata.InputBoxComponent; + private controllerEndpointRow!: azdata.FlexContainer; + private sqlServerDNSInput!: azdata.InputBoxComponent; + private sqlServerEndpointRow!: azdata.FlexContainer; + private sqlServerPortInput!: azdata.InputBoxComponent; + private gatewayDNSInput!: azdata.InputBoxComponent; + private gatewayPortInput!: azdata.InputBoxComponent; + private gatewayEndpointRow!: azdata.FlexContainer; + private readableSecondaryDNSInput!: azdata.InputBoxComponent; + private readableSecondaryPortInput!: azdata.InputBoxComponent; + private readableSecondaryEndpointRow!: azdata.FlexContainer; + private endpointNameColumnHeader!: azdata.TextComponent; + private controllerNameLabel!: azdata.TextComponent; + private SqlServerNameLabel!: azdata.TextComponent; + private gatewayNameLabel!: azdata.TextComponent; + private readableSecondaryNameLabel!: azdata.TextComponent; + + constructor(wizard: DeployClusterWizard) { + super(localize('deployCluster.ServiceSettingsPageTitle', "Service settings"), '', wizard); + } + public initialize(): void { + const scaleSectionInfo: SectionInfo = { + title: localize('deployCluster.scaleSectionTitle', "Scale settings"), + labelWidth: labelWidth, + inputWidth: inputWidth, + spaceBetweenFields: spaceBetweenFields, + rows: [{ + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.ComputeText', "Compute"), + min: 1, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.ComputePoolScale_VariableName, + } + ] + }, { + fields: [{ + type: FieldType.Number, + label: localize('deployCluster.DataText', "Data"), + min: 1, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.DataPoolScale_VariableName, + }] + }, { + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.HDFSText', "HDFS"), + min: 1, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.HDFSPoolScale_VariableName + }, { + type: FieldType.Checkbox, + label: localize('deployCluster.includeSparkInHDFSPool', "Include Spark"), + defaultValue: 'true', + variableName: VariableNames.IncludeSpark_VariableName, + required: false + } + ] + }, { + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.SparkText', "Spark"), + min: 0, + max: 100, + defaultValue: '0', + useCustomValidator: true, + required: true, + variableName: VariableNames.SparkPoolScale_VariableName + } + ] + } + ] + }; + + const hadrSectionInfo: SectionInfo = { + title: localize('deployCluster.HadrSection', "High availability settings"), + labelWidth: labelWidth, + inputWidth: inputWidth, + spaceBetweenFields: spaceBetweenFields, + rows: [{ + fields: [ + { + type: FieldType.Options, + label: localize('deployCluster.MasterSqlText', "SQL Server Master"), + options: ['1', '3', '4', '5', '6', '7', '8', '9'], + defaultValue: '1', + required: true, + variableName: VariableNames.SQLServerScale_VariableName, + }, { + type: FieldType.Checkbox, + label: localize('deployCluster.EnableHADR', "Enable Availability Groups"), + defaultValue: 'false', + variableName: VariableNames.EnableHADR_VariableName, + required: false + } + ] + }, { + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.HDFSNameNodeText', "HDFS name node"), + min: 1, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.HDFSNameNodeScale_VariableName + } + ] + }, { + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.SparkHeadText', "SparkHead"), + min: 0, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.SparkHeadScale_VariableName + } + ] + }, { + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.ZooKeeperText', "ZooKeeper"), + min: 0, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.ZooKeeperScale_VariableName + } + ] + } + ] + }; + + const hintTextForStorageFields = localize('deployCluster.storageFieldTooltip', "Use controller settings"); + const storageSectionInfo: SectionInfo = { + title: '', + labelWidth: '0px', + inputWidth: inputWidth, + spaceBetweenFields: spaceBetweenFields, + rows: [{ + fields: [ + { + type: FieldType.ReadonlyText, + label: '', + required: false, + defaultValue: localize('deployCluster.DataStorageClassName', "Storage class for data"), + variableName: '', + labelWidth: labelWidth + }, { + type: FieldType.ReadonlyText, + label: '', + required: false, + defaultValue: localize('deployCluster.DataClaimSize', "Claim size for data (GB)"), + variableName: '' + }, { + type: FieldType.ReadonlyText, + label: '', + required: false, + defaultValue: localize('deployCluster.LogStorageClassName', "Storage class for logs"), + variableName: '', + }, { + type: FieldType.ReadonlyText, + label: '', + required: false, + defaultValue: localize('deployCluster.LogsClaimSize', "Claim size for logs (GB)"), + variableName: '' + } + ] + }, + { + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.ControllerText', "Controller"), + useCustomValidator: true, + variableName: VariableNames.ControllerDataStorageClassName_VariableName, + required: true, + description: localize('deployCluster.AdvancedStorageDescription', "By default Controller storage settings will be applied to other services as well, you can expand the advanced storage settings to configure storage for other services."), + labelWidth: labelWidth + }, { + type: FieldType.Number, + label: '', + useCustomValidator: true, + min: 1, + variableName: VariableNames.ControllerDataStorageSize_VariableName, + }, { + type: FieldType.Text, + label: '', + useCustomValidator: true, + min: 1, + variableName: VariableNames.ControllerLogsStorageClassName_VariableName, + }, { + type: FieldType.Number, + label: '', + useCustomValidator: true, + min: 1, + variableName: VariableNames.ControllerLogsStorageSize_VariableName, + } + ] + } + ] + }; + const advancedStorageSectionInfo: SectionInfo = { + title: localize('deployCluster.AdvancedStorageSectionTitle', "Advanced storage settings"), + labelWidth: '0px', + inputWidth: inputWidth, + spaceBetweenFields: spaceBetweenFields, + collapsible: true, + collapsed: true, + rows: [{ + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.HDFSText', "HDFS"), + required: false, + variableName: VariableNames.HDFSDataStorageClassName_VariableName, + placeHolder: hintTextForStorageFields, + labelWidth: labelWidth + }, { + type: FieldType.Number, + label: '', + required: false, + min: 1, + variableName: VariableNames.HDFSDataStorageSize_VariableName, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Text, + label: '', + required: false, + variableName: VariableNames.HDFSLogsStorageClassName_VariableName, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Number, + label: '', + required: false, + min: 1, + variableName: VariableNames.HDFSLogsStorageSize_VariableName, + placeHolder: hintTextForStorageFields + } + ] + }, { + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.DataText', "Data"), + required: false, + variableName: VariableNames.DataPoolDataStorageClassName_VariableName, + labelWidth: labelWidth, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Number, + label: '', + required: false, + min: 1, + variableName: VariableNames.DataPoolDataStorageSize_VariableName, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Text, + label: '', + required: false, + variableName: VariableNames.DataPoolLogsStorageClassName_VariableName, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Number, + label: '', + required: false, + min: 1, + variableName: VariableNames.DataPoolLogsStorageSize_VariableName, + placeHolder: hintTextForStorageFields + } + ] + }, { + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.MasterSqlText', "SQL Server Master"), + required: false, + variableName: VariableNames.SQLServerDataStorageClassName_VariableName, + labelWidth: labelWidth, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Number, + label: '', + required: false, + min: 1, + variableName: VariableNames.SQLServerDataStorageSize_VariableName, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Text, + label: '', + required: false, + variableName: VariableNames.SQLServerLogsStorageClassName_VariableName, + placeHolder: hintTextForStorageFields + }, { + type: FieldType.Number, + label: '', + required: false, + min: 1, + variableName: VariableNames.SQLServerLogsStorageSize_VariableName, + placeHolder: hintTextForStorageFields + } + ] + }] + }; + + this.pageObject.registerContent((view: azdata.ModelView) => { + const createSectionFunc = (sectionInfo: SectionInfo): azdata.GroupContainer => { + return createSection({ + view: view, + container: this.wizard.wizardObject, + sectionInfo: sectionInfo, + onNewDisposableCreated: (disposable: vscode.Disposable): void => { + this.wizard.registerDisposable(disposable); + }, + onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { + this.inputComponents[name] = component; + }, + onNewValidatorCreated: (validator: Validator): void => { + } + }); + }; + const scaleSection = createSectionFunc(scaleSectionInfo); + const hadrSection = createSectionFunc(hadrSectionInfo); + const endpointSection = this.createEndpointSection(view); + const storageSection = createSectionFunc(storageSectionInfo); + const advancedStorageSection = createSectionFunc(advancedStorageSectionInfo); + const storageContainer = createGroupContainer(view, [storageSection, advancedStorageSection], { + header: localize('deployCluster.StorageSectionTitle', "Storage settings"), + collapsible: true + }); + this.setSQLServerMasterFieldEventHandler(); + const form = view.modelBuilder.formContainer().withFormItems([ + { + title: '', + component: scaleSection + }, { + title: '', + component: hadrSection + }, { + title: '', + component: endpointSection + }, { + title: '', + component: storageContainer + } + ]).withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); + } + + private createEndpointSection(view: azdata.ModelView): azdata.GroupContainer { + this.endpointNameColumnHeader = createLabel(view, { text: '', width: labelWidth }); + this.dnsColumnHeader = createLabel(view, { text: localize('deployCluster.DNSNameHeader', "DNS name"), width: inputWidth }); + this.portColumnHeader = createLabel(view, { text: localize('deployCluster.PortHeader', "Port"), width: PortInputWidth }); + this.endpointHeaderRow = createFlexContainer(view, [this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]); + + this.controllerNameLabel = createLabel(view, { text: localize('deployCluster.ControllerText', "Controller"), width: labelWidth, required: true }); + this.controllerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth }); + this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: PortInputWidth, min: 1 }); + this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]); + this.inputComponents[VariableNames.ControllerDNSName_VariableName] = this.controllerDNSInput; + this.inputComponents[VariableNames.ControllerPort_VariableName] = this.controllerPortInput; + + this.SqlServerNameLabel = createLabel(view, { text: localize('deployCluster.MasterSqlText', "SQL Server Master"), width: labelWidth, required: true }); + this.sqlServerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth }); + this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: PortInputWidth, min: 1 }); + this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]); + this.inputComponents[VariableNames.SQLServerDNSName_VariableName] = this.sqlServerDNSInput; + this.inputComponents[VariableNames.SQLServerPort_VariableName] = this.sqlServerPortInput; + + this.gatewayNameLabel = createLabel(view, { text: localize('deployCluster.GatewayText', "Gateway"), width: labelWidth, required: true }); + this.gatewayDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth }); + this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: PortInputWidth, min: 1 }); + this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]); + this.inputComponents[VariableNames.GatewayDNSName_VariableName] = this.gatewayDNSInput; + this.inputComponents[VariableNames.GateWayPort_VariableName] = this.gatewayPortInput; + + this.readableSecondaryNameLabel = createLabel(view, { text: localize('deployCluster.ReadableSecondaryText', "Readable secondary"), width: labelWidth, required: true }); + this.readableSecondaryDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth }); + this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: PortInputWidth, min: 1 }); + this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]); + this.inputComponents[VariableNames.ReadableSecondaryDNSName_VariableName] = this.readableSecondaryDNSInput; + this.inputComponents[VariableNames.ReadableSecondaryPort_VariableName] = this.readableSecondaryPortInput; + + return createGroupContainer(view, [this.endpointHeaderRow, this.controllerEndpointRow, this.sqlServerEndpointRow, this.gatewayEndpointRow, this.readableSecondaryEndpointRow], { + header: localize('deployCluster.EndpointSettings', "Endpoint settings"), + collapsible: true + }); + } + + public onEnter(): void { + this.setDropdownValue(VariableNames.SQLServerScale_VariableName); + this.setCheckboxValue(VariableNames.EnableHADR_VariableName); + this.setInputBoxValue(VariableNames.ComputePoolScale_VariableName); + this.setInputBoxValue(VariableNames.DataPoolScale_VariableName); + this.setInputBoxValue(VariableNames.HDFSPoolScale_VariableName); + this.setInputBoxValue(VariableNames.HDFSNameNodeScale_VariableName); + this.setInputBoxValue(VariableNames.SparkPoolScale_VariableName); + this.setInputBoxValue(VariableNames.SparkHeadScale_VariableName); + this.setInputBoxValue(VariableNames.ZooKeeperScale_VariableName); + this.setCheckboxValue(VariableNames.IncludeSpark_VariableName); + this.setEnableHadrCheckboxState(this.wizard.model.getIntegerValue(VariableNames.SQLServerScale_VariableName)); + this.setInputBoxValue(VariableNames.ControllerPort_VariableName); + this.setInputBoxValue(VariableNames.SQLServerPort_VariableName); + this.setInputBoxValue(VariableNames.GateWayPort_VariableName); + this.setInputBoxValue(VariableNames.ReadableSecondaryPort_VariableName); + + this.setInputBoxValue(VariableNames.ControllerDataStorageClassName_VariableName); + this.setInputBoxValue(VariableNames.ControllerDataStorageSize_VariableName); + this.setInputBoxValue(VariableNames.ControllerLogsStorageClassName_VariableName); + this.setInputBoxValue(VariableNames.ControllerLogsStorageSize_VariableName); + + this.endpointHeaderRow.clearItems(); + if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { + this.endpointHeaderRow.addItems([this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]); + } + this.loadEndpointRow(this.controllerEndpointRow, this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput); + this.loadEndpointRow(this.gatewayEndpointRow, this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput); + this.loadEndpointRow(this.sqlServerEndpointRow, this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput); + this.updateReadableSecondaryEndpointComponents(this.wizard.model.hadrEnabled); + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + this.wizard.wizardObject.message = { text: '' }; + if (pcInfo.newPage > pcInfo.lastPage) { + const isValid: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ComputePoolScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DataPoolScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.HDFSNameNodeScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.HDFSPoolScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.SparkPoolScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.SparkHeadScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ZooKeeperScale_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerDataStorageClassName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerDataStorageSize_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerLogsStorageClassName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerLogsStorageSize_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerPort_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.SQLServerPort_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.GateWayPort_VariableName, this.inputComponents)) + && (!getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents).checked + || !isInputBoxEmpty(this.readableSecondaryPortInput)) + && (this.wizard.model.authenticationMode !== AuthenticationMode.ActiveDirectory + || (!isInputBoxEmpty(this.gatewayDNSInput) + && !isInputBoxEmpty(this.controllerDNSInput) + && !isInputBoxEmpty(this.sqlServerDNSInput) + && !isInputBoxEmpty(this.readableSecondaryDNSInput) + )); + if (!isValid) { + this.wizard.wizardObject.message = { + text: MissingRequiredInformationErrorMessage, + level: azdata.window.MessageLevel.Error + }; + } + return isValid; + } + return true; + }); + } + + public onLeave(): void { + setModelValues(this.inputComponents, this.wizard.model); + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + private setInputBoxValue(variableName: string): void { + getInputBoxComponent(variableName, this.inputComponents).value = this.wizard.model.getStringValue(variableName); + } + + private setCheckboxValue(variableName: string): void { + getCheckboxComponent(variableName, this.inputComponents).checked = this.wizard.model.getBooleanValue(variableName); + } + + private setDropdownValue(variableName: string): void { + getDropdownComponent(variableName, this.inputComponents).value = this.wizard.model.getStringValue(variableName); + } + + private setSQLServerMasterFieldEventHandler() { + const sqlScaleDropdown = getDropdownComponent(VariableNames.SQLServerScale_VariableName, this.inputComponents); + const enableHadrCheckbox = getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents); + this.wizard.registerDisposable(sqlScaleDropdown.onValueChanged(() => { + const selectedValue = typeof sqlScaleDropdown.value === 'string' ? sqlScaleDropdown.value : sqlScaleDropdown.value!.name; + this.setEnableHadrCheckboxState(Number.parseInt(selectedValue)); + })); + this.wizard.registerDisposable(enableHadrCheckbox.onChanged(() => { + this.updateReadableSecondaryEndpointComponents(enableHadrCheckbox.checked); + })); + } + + private setEnableHadrCheckboxState(sqlInstances: number) { + // 1. it is ok to enable HADR when there is only 1 replica + // 2. if there are multiple replicas, the hadr.enabled switch must be set to true. + const enableHadrCheckbox = getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents); + const hadrEnabled = sqlInstances === 1 ? enableHadrCheckbox.checked : true; + if (sqlInstances === 1) { + enableHadrCheckbox.enabled = true; + } else { + enableHadrCheckbox.enabled = false; + } + enableHadrCheckbox.checked = hadrEnabled; + this.updateReadableSecondaryEndpointComponents(hadrEnabled); + } + + private updateReadableSecondaryEndpointComponents(hadrEnabled: boolean) { + this.readableSecondaryEndpointRow.clearItems(); + if (hadrEnabled) { + this.loadEndpointRow(this.readableSecondaryEndpointRow, this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput); + } + } + + private loadEndpointRow(row: azdata.FlexContainer, label: azdata.TextComponent, dnsInput: azdata.InputBoxComponent, portInput: azdata.InputBoxComponent): void { + row.clearItems(); + const itemLayout: azdata.FlexItemLayout = { CSSStyles: { 'margin-right': '20px' } }; + row.addItem(label); + if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { + row.addItem(dnsInput, itemLayout); + } + row.addItem(portInput); + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts new file mode 100644 index 0000000000..43b662bf9b --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts @@ -0,0 +1,415 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +import * as vscode from 'vscode'; +import { DeployClusterWizard } from '../deployClusterWizard'; +import { SectionInfo, FieldType, LabelPosition, FontStyle, BdcDeploymentType } from '../../../interfaces'; +import { createSection, createGroupContainer, createFlexContainer, createLabel } from '../../modelViewUtils'; +import { WizardPageBase } from '../../wizardPageBase'; +import * as VariableNames from '../constants'; +import * as os from 'os'; +import { join } from 'path'; +import * as fs from 'fs'; +import { AuthenticationMode } from '../deployClusterWizardModel'; +import { BigDataClusterDeploymentProfile } from '../../../services/bigDataClusterDeploymentProfile'; +const localize = nls.loadMessageBundle(); + +export class SummaryPage extends WizardPageBase { + private formItems: azdata.FormComponent[] = []; + private form!: azdata.FormBuilder; + private view!: azdata.ModelView; + private targetDeploymentProfile!: BigDataClusterDeploymentProfile; + + constructor(wizard: DeployClusterWizard) { + super(localize('deployCluster.summaryPageTitle', "Summary"), '', wizard); + } + + public initialize(): void { + this.pageObject.registerContent((view: azdata.ModelView) => { + this.view = view; + const deploymentJsonSection = createGroupContainer(view, [ + view.modelBuilder.flexContainer().withItems([ + this.createSaveJsonButton(localize('deployCluster.SaveBdcJson', "Save bdc.json"), 'bdc.json', () => { return this.targetDeploymentProfile.getBdcJson(); }), + this.createSaveJsonButton(localize('deployCluster.SaveControlJson', "Save control.json"), 'control.json', () => { return this.targetDeploymentProfile.getControlJson(); }) + ], { + CSSStyles: { 'margin-right': '10px' } + }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component() + ], { + header: localize('deployCluster.DeploymentJSON', "Deployment JSON files"), + collapsible: true + }); + + this.form = view.modelBuilder.formContainer().withFormItems([ + { + title: '', + component: deploymentJsonSection + } + ]); + return view.initializeModel(this.form!.withLayout({ width: '100%' }).component()); + }); + } + + public onEnter() { + this.targetDeploymentProfile = this.wizard.model.createTargetProfile(); + this.formItems.forEach(item => { + this.form!.removeFormItem(item); + }); + this.formItems = []; + + const deploymentTargetSectionInfo: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: '150px', + inputWidth: '200px', + title: localize('deployCluster.DeploymentTarget', "Deployment target"), + rows: [ + { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.Kubeconfig', "Kube config"), + defaultValue: this.wizard.model.getStringValue(VariableNames.KubeConfigPath_VariableName), + fontStyle: FontStyle.Italic + }, + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ClusterContext', "Cluster context"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterContext_VariableName), + fontStyle: FontStyle.Italic + }] + } + ] + }; + + const clusterSectionInfo: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: '150px', + inputWidth: '200px', + title: localize('deployCluster.ClusterSettings', "Cluster settings"), + rows: [ + { + + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.DeploymentProfile', "Deployment profile"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName), + fontStyle: FontStyle.Italic + }, + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ClusterName', "Cluster name"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterName_VariableName), + fontStyle: FontStyle.Italic + }] + }, { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ControllerUsername', "Controller username"), + defaultValue: this.wizard.model.getStringValue(VariableNames.AdminUserName_VariableName), + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.AuthenticationMode', "Authentication mode"), + defaultValue: this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory ? + localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") : + localize('deployCluster.AuthenticationMode.Basic', "Basic"), + fontStyle: FontStyle.Italic + } + ] + } + ] + }; + + const azureSectionInfo: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: '150px', + inputWidth: '200px', + title: localize('deployCluster.AzureSettings', "Azure settings"), + rows: [{ + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.SubscriptionId', "Subscription id"), + defaultValue: this.wizard.model.getStringValue(VariableNames.SubscriptionId_VariableName) || localize('deployCluster.DefaultSubscription', "Default Azure Subscription"), + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ResourceGroup', "Resource group"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ResourceGroup_VariableName), + fontStyle: FontStyle.Italic + } + ] + }, { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.Region', "Region"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName), + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.AksClusterName', "AKS cluster name"), + defaultValue: this.wizard.model.getStringValue(VariableNames.AksName_VariableName), + fontStyle: FontStyle.Italic + } + ] + }, { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.VMSize', "VM size"), + defaultValue: this.wizard.model.getStringValue(VariableNames.VMSize_VariableName), + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.VMCount', "VM count"), + defaultValue: this.wizard.model.getStringValue(VariableNames.VMCount_VariableName), + fontStyle: FontStyle.Italic + } + ] + } + ] + }; + + const scaleSectionInfo: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: '150px', + inputWidth: '200px', + title: localize('deployCluster.ScaleSettings', "Scale settings"), + rows: [ + { + fields: [{ + type: FieldType.ReadonlyText, + label: localize('deployCluster.ComputeText', "Compute"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName), + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.DataText', "Data"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DataPoolScale_VariableName), + fontStyle: FontStyle.Italic + } + ] + }, { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.HDFSText', "HDFS"), + defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`, + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.SparkText', "Spark"), + defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName), + fontStyle: FontStyle.Italic + } + ] + } + ] + }; + + const hadrSectionInfo: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: '150px', + inputWidth: '200px', + title: localize('deployCluster.HadrSection', "High availability settings"), + rows: [ + { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.SqlServerText', "SQL Server Master"), + defaultValue: `${this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName)} ${this.wizard.model.hadrEnabled ? localize('deployCluster.WithHADR', "(Availability Groups Enabled)") : ''}`, + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.HDFSNameNodeText', "HDFS name node"), + defaultValue: this.wizard.model.getStringValue(VariableNames.HDFSNameNodeScale_VariableName), + fontStyle: FontStyle.Italic + } + ] + }, { + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ZooKeeperText', "ZooKeeper"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ZooKeeperScale_VariableName), + fontStyle: FontStyle.Italic + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.SparkHeadText', "SparkHead"), + defaultValue: this.wizard.model.getStringValue(VariableNames.SparkHeadScale_VariableName), + fontStyle: FontStyle.Italic + } + ] + } + ] + }; + + const createSectionFunc = (sectionInfo: SectionInfo): azdata.FormComponent => { + return { + title: '', + component: createSection({ + container: this.wizard.wizardObject, + sectionInfo: sectionInfo, + view: this.view, + onNewDisposableCreated: () => { }, + onNewInputComponentCreated: () => { }, + onNewValidatorCreated: () => { } + }) + }; + }; + + if (this.wizard.deploymentType === BdcDeploymentType.ExistingAKS || this.wizard.deploymentType === BdcDeploymentType.ExistingKubeAdm) { + const deploymentTargetSection = createSectionFunc(deploymentTargetSectionInfo); + this.formItems.push(deploymentTargetSection); + } + + const clusterSection = createSectionFunc(clusterSectionInfo); + const scaleSection = createSectionFunc(scaleSectionInfo); + const hadrSection = createSectionFunc(hadrSectionInfo); + const endpointSection = { + title: '', + component: this.createEndpointSection() + }; + const storageSection = { + title: '', + component: this.createStorageSection() + }; + if (this.wizard.model.getStringValue(VariableNames.AksName_VariableName)) { + const azureSection = createSectionFunc(azureSectionInfo); + this.formItems.push(azureSection); + } + + this.formItems.push(clusterSection, scaleSection, hadrSection, endpointSection, storageSection); + this.form.addFormItems(this.formItems); + } + + private getStorageSettingValue(propertyName: string, defaultValuePropertyName: string): string | undefined { + const value = this.wizard.model.getStringValue(propertyName); + return (value === undefined || value === '') ? this.wizard.model.getStringValue(defaultValuePropertyName) : value; + } + + private createStorageSection(): azdata.GroupContainer { + const serviceNameColumn: azdata.TableColumn = { + value: ' ', + width: 150 + }; + const dataStorageClassColumn: azdata.TableColumn = { + value: localize('deployCluster.DataStorageClassName', "Storage class for data"), + width: 180 + }; + const dataStorageSizeColumn: azdata.TableColumn = { + value: localize('deployCluster.DataClaimSize', "Claim size for data (GB)"), + width: 180 + }; + const logStorageClassColumn: azdata.TableColumn = { + value: localize('deployCluster.LogStorageClassName', "Storage class for logs"), + width: 180 + }; + const logStorageSizeColumn: azdata.TableColumn = { + value: localize('deployCluster.LogsClaimSize', "Claim size for logs (GB)"), + width: 180 + }; + const storageTable = this.view.modelBuilder.table().withProperties({ + data: [ + [ + localize('deployCluster.ControllerText', "Controller"), + this.wizard.model.getStringValue(VariableNames.ControllerDataStorageClassName_VariableName), + this.wizard.model.getStringValue(VariableNames.ControllerDataStorageSize_VariableName), + this.wizard.model.getStringValue(VariableNames.ControllerLogsStorageClassName_VariableName), + this.wizard.model.getStringValue(VariableNames.ControllerLogsStorageSize_VariableName)], + [ + localize('deployCluster.HDFSText', "HDFS"), + this.getStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName), + this.getStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName), + this.getStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName), + this.getStorageSettingValue(VariableNames.HDFSLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName) + ], [ + localize('deployCluster.DataText', "Data"), + this.getStorageSettingValue(VariableNames.DataPoolDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName), + this.getStorageSettingValue(VariableNames.DataPoolDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName), + this.getStorageSettingValue(VariableNames.DataPoolLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName), + this.getStorageSettingValue(VariableNames.DataPoolLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName) + ], [ + localize('deployCluster.MasterSqlText', "SQL Server Master"), + this.getStorageSettingValue(VariableNames.SQLServerDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName), + this.getStorageSettingValue(VariableNames.SQLServerDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName), + this.getStorageSettingValue(VariableNames.SQLServerLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName), + this.getStorageSettingValue(VariableNames.SQLServerLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName) + ] + ], + columns: [serviceNameColumn, dataStorageClassColumn, dataStorageSizeColumn, logStorageClassColumn, logStorageSizeColumn], + width: '1000px', + height: '140px' + }).component(); + return createGroupContainer(this.view, [storageTable], { + header: localize('deployCluster.StorageSettings', "Storage settings"), + collapsible: true + }); + } + + private createEndpointSection(): azdata.GroupContainer { + const endpointRows = [ + this.createEndpointRow(localize('deployCluster.ControllerText', "Controller"), VariableNames.ControllerDNSName_VariableName, VariableNames.ControllerPort_VariableName), + this.createEndpointRow(localize('deployCluster.SqlServerText', "SQL Server Master"), VariableNames.SQLServerDNSName_VariableName, VariableNames.SQLServerPort_VariableName), + this.createEndpointRow(localize('deployCluster.GatewayText', "Gateway"), VariableNames.GatewayDNSName_VariableName, VariableNames.GateWayPort_VariableName) + ]; + + if (this.wizard.model.hadrEnabled) { + endpointRows.push( + this.createEndpointRow(localize('deployCluster.ReadableSecondaryText', "Readable secondary"), VariableNames.ReadableSecondaryDNSName_VariableName, VariableNames.ReadableSecondaryPort_VariableName) + ); + } + return createGroupContainer(this.view, endpointRows, { + header: localize('deployCluster.EndpointSettings', "Endpoint settings"), + collapsible: true + }); + } + + private createEndpointRow(name: string, dnsVariableName: string, portVariableName: string): azdata.FlexContainer { + const items = []; + items.push(createLabel(this.view, { text: name, width: '150px' })); + if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { + items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px', fontStyle: FontStyle.Italic })); + } + items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(portVariableName)!, width: '100px', fontStyle: FontStyle.Italic })); + return createFlexContainer(this.view, items); + } + + private createSaveJsonButton(label: string, fileName: string, getContent: () => string): azdata.ButtonComponent { + const button = this.view.modelBuilder.button().withProperties({ + title: label, + label: fileName, + ariaLabel: label, + width: '150px' + }).component(); + this.wizard.registerDisposable(button.onDidClick(() => { + vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(join(os.homedir(), fileName)), + filters: { + 'JSON': ['json'] + } + }).then((path) => { + if (path) { + fs.promises.writeFile(path.fsPath, getContent()).then(() => { + this.wizard.wizardObject.message = { + text: localize('deployCluster.SaveJsonFileMessage', "File saved: {0}", path.fsPath), + level: azdata.window.MessageLevel.Information + }; + }).catch((error) => { + this.wizard.wizardObject.message = { + text: error.message, + level: azdata.window.MessageLevel.Error + }; + }); + } + }); + })); + return button; + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/targetClusterPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/targetClusterPage.ts new file mode 100644 index 0000000000..6efdc7f3dd --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/targetClusterPage.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * 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 os from 'os'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { DeployClusterWizard } from '../deployClusterWizard'; +import { WizardPageBase } from '../../wizardPageBase'; +import { KubeClusterContext } from '../../../services/kubeService'; +import { ClusterContext_VariableName, KubeConfigPath_VariableName } from '../constants'; +const localize = nls.loadMessageBundle(); + +const ClusterRadioButtonGroupName = 'ClusterRadioGroup'; + +export class TargetClusterContextPage extends WizardPageBase { + private existingClusterControl: azdata.FlexContainer | undefined; + private clusterContextsLabel: azdata.TextComponent | undefined; + private errorLoadingClustersLabel: azdata.TextComponent | undefined; + private clusterContextList: azdata.DivContainer | undefined; + private clusterContextLoadingComponent: azdata.LoadingComponent | undefined; + private configFileInput: azdata.InputBoxComponent | undefined; + private browseFileButton: azdata.ButtonComponent | undefined; + private loadDefaultKubeConfigFile: boolean = true; + private view: azdata.ModelView | undefined; + + constructor(wizard: DeployClusterWizard) { + super(localize('deployCluster.TargetClusterContextPageTitle', "Target cluster context"), + localize('deployCluster.TargetClusterContextPageDescription', "Select the kube config file and then select a cluster context from the list"), wizard); + } + + public initialize(): void { + this.pageObject.registerContent((view: azdata.ModelView) => { + this.view = view; + this.initExistingClusterControl(); + let formBuilder = view.modelBuilder.formContainer().withFormItems( + [ + { + component: this.existingClusterControl!, + title: '' + } + ], + { + horizontal: false + } + ).withLayout({ width: '100%', height: '100%' }); + const form = formBuilder.withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); + } + + public onEnter() { + if (this.loadDefaultKubeConfigFile) { + let defaultKubeConfigPath = this.wizard.kubeService.getDefautConfigPath(); + this.loadClusterContexts(defaultKubeConfigPath); + this.loadDefaultKubeConfigFile = false; + } + + this.wizard.wizardObject.registerNavigationValidator((e) => { + if (e.lastPage > e.newPage) { + this.wizard.wizardObject.message = { text: '' }; + return true; + } + let clusterSelected = this.wizard.model.getStringValue(ClusterContext_VariableName) !== undefined; + if (!clusterSelected) { + this.wizard.wizardObject.message = { + text: localize('deployCluster.ClusterContextNotSelectedMessage', 'Please select a cluster context.'), + level: azdata.window.MessageLevel.Error + }; + } + return clusterSelected; + }); + } + + public onLeave() { + this.wizard.wizardObject.registerNavigationValidator((e) => { + return true; + }); + } + + private initExistingClusterControl(): void { + let self = this; + const labelWidth = '150px'; + let configFileLabel = this.view!.modelBuilder.text().withProperties({ value: localize('deployCluster.kubeConfigFileLabelText', 'Kube config file path') }).component(); + configFileLabel.width = labelWidth; + this.configFileInput = this.view!.modelBuilder.inputBox().withProperties({ width: '300px' }).component(); + this.configFileInput.enabled = false; + this.browseFileButton = this.view!.modelBuilder.button().withProperties({ label: localize('deployCluster.browseText', 'Browse'), width: '100px' }).component(); + let configFileContainer = this.view!.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'row', alignItems: 'baseline' }) + .withItems([configFileLabel, this.configFileInput, this.browseFileButton], { CSSStyles: { 'margin-right': '10px' } }).component(); + this.clusterContextsLabel = this.view!.modelBuilder.text().withProperties({ value: localize('deployCluster.clusterContextsLabelText', 'Cluster Contexts') }).component(); + this.clusterContextsLabel.width = labelWidth; + this.errorLoadingClustersLabel = this.view!.modelBuilder.text().withProperties({ value: localize('deployCluster.errorLoadingClustersText', 'No cluster information is found in the config file or an error ocurred while loading the config file') }).component(); + this.clusterContextList = this.view!.modelBuilder.divContainer().component(); + this.clusterContextLoadingComponent = this.view!.modelBuilder.loadingComponent().withItem(this.clusterContextList).component(); + this.existingClusterControl = this.view!.modelBuilder.divContainer().withProperties({ clickable: false }).component(); + let clusterContextContainer = this.view!.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'start' }).component(); + clusterContextContainer.addItem(this.clusterContextsLabel, { flex: '0 0 auto' }); + clusterContextContainer.addItem(this.clusterContextLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'width': '400px', 'margin-left': '10px', 'margin-top': '10px' } }); + + this.existingClusterControl.addItem(configFileContainer, { CSSStyles: { 'margin-top': '0px' } }); + this.existingClusterControl.addItem(clusterContextContainer, { + CSSStyles: { 'margin- top': '10px' } + }); + + this.wizard.registerDisposable(this.browseFileButton.onDidClick(async () => { + let fileUris = await vscode.window.showOpenDialog( + { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + defaultUri: vscode.Uri.file(os.homedir()), + openLabel: localize('deployCluster.selectKubeConfigFileText', 'Select'), + filters: { + 'Config Files': ['*'], + } + } + ); + + if (!fileUris || fileUris.length === 0) { + return; + } + self.clusterContextList!.clearItems(); + + let fileUri = fileUris[0]; + + self.loadClusterContexts(fileUri.fsPath); + })); + } + + private async loadClusterContexts(configPath: string): Promise { + this.clusterContextLoadingComponent!.loading = true; + this.wizard.model.setPropertyValue(ClusterContext_VariableName, undefined); + this.wizard.wizardObject.message = { text: '' }; + let self = this; + this.configFileInput!.value = configPath; + + let clusterContexts: KubeClusterContext[] = []; + try { + clusterContexts = await this.wizard.kubeService.getClusterContexts(configPath); + } catch (error) { + this.wizard.wizardObject.message = { + text: localize('deployCluster.ConfigParseError', "Failed to load the config file"), + description: error.message || error, level: azdata.window.MessageLevel.Error + }; + } + if (clusterContexts.length !== 0) { + self.wizard.model.setPropertyValue(KubeConfigPath_VariableName, configPath); + let options = clusterContexts.map(clusterContext => { + let option = this.view!.modelBuilder.radioButton().withProperties({ + label: clusterContext.name, + checked: clusterContext.isCurrentContext, + name: ClusterRadioButtonGroupName + }).component(); + + if (clusterContext.isCurrentContext) { + self.wizard.model.setPropertyValue(ClusterContext_VariableName, clusterContext.name); + self.wizard.wizardObject.message = { text: '' }; + } + + this.wizard.registerDisposable(option.onDidClick(() => { + self.wizard.model.setPropertyValue(ClusterContext_VariableName, clusterContext.name); + self.wizard.wizardObject.message = { text: '' }; + })); + return option; + }); + self.clusterContextList!.addItems(options); + } else { + self.clusterContextList!.addItem(this.errorLoadingClustersLabel!); + } + this.clusterContextLoadingComponent!.loading = false; + } +} diff --git a/extensions/resource-deployment/src/ui/dialogBase.ts b/extensions/resource-deployment/src/ui/dialogBase.ts index 0a31dd9dc0..d6db0ecbf9 100644 --- a/extensions/resource-deployment/src/ui/dialogBase.ts +++ b/extensions/resource-deployment/src/ui/dialogBase.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; @@ -16,10 +15,10 @@ export abstract class DialogBase { this._dialogObject.cancelButton.onClick(() => this.onCancel()); } - protected abstract initializeDialog(): void; + protected abstract initialize(): void; public open(): void { - this.initializeDialog(); + this.initialize(); azdata.window.openDialog(this._dialogObject); } diff --git a/extensions/resource-deployment/src/ui/model.ts b/extensions/resource-deployment/src/ui/model.ts new file mode 100644 index 0000000000..8c61b6adf3 --- /dev/null +++ b/extensions/resource-deployment/src/ui/model.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class Model { + private propValueObject: { [s: string]: string | undefined } = {}; + + public setPropertyValue(property: string, value: string | number | boolean | undefined): void { + if (typeof value === 'boolean') { + this.propValueObject[property] = value ? 'true' : 'false'; + } else if (typeof value === 'number') { + this.propValueObject[property] = value.toString(); + } else { + this.propValueObject[property] = value; + } + } + + public getIntegerValue(propName: string, defaultValue: number = 0): number { + const value = this.propValueObject[propName]; + return value === undefined ? defaultValue : Number.parseInt(value); + } + + public getStringValue(propName: string, defaultValue?: string): string | undefined { + const value = this.propValueObject[propName]; + return value === undefined ? defaultValue : value; + } + + public getBooleanValue(propName: string, defaultValue: boolean = false): boolean { + const value = this.propValueObject[propName]; + return value === undefined ? defaultValue : value === 'true'; + } + + public setEnvironmentVariables(): void { + Object.keys(this.propValueObject).filter(propertyName => propertyName.startsWith('AZDATA_NB_VAR_')).forEach(propertyName => { + const value = this.getStringValue(propertyName); + if (value !== undefined && value !== '') { + process.env[propertyName] = value; + } + process.env[propertyName] = value === undefined ? '' : value; + }); + } +} diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts new file mode 100644 index 0000000000..cefdc923b3 --- /dev/null +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import { DialogInfo, FieldType, FieldInfo, SectionInfo, LabelPosition } from '../interfaces'; +import { Model } from './model'; + +const localize = nls.loadMessageBundle(); + +export type Validator = () => { valid: boolean, message: string }; +export type InputComponents = { [s: string]: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent; }; + +export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent { + return inputComponents[name]; +} + +export function getDropdownComponent(name: string, inputComponents: InputComponents): azdata.DropDownComponent { + return inputComponents[name]; +} + +export function getCheckboxComponent(name: string, inputComponents: InputComponents): azdata.CheckBoxComponent { + return inputComponents[name]; +} + +export const DefaultInputComponentWidth = '400px'; +export const DefaultLabelComponentWidth = '200px'; + +export interface DialogContext extends CreateContext { + dialogInfo: DialogInfo; + container: azdata.window.Dialog; +} + +export interface WizardPageContext extends CreateContext { + sections: SectionInfo[]; + page: azdata.window.WizardPage; + container: azdata.window.Wizard; +} + + +export interface SectionContext extends CreateContext { + sectionInfo: SectionInfo; + view: azdata.ModelView; +} + +interface FieldContext extends CreateContext { + fieldInfo: FieldInfo; + components: azdata.Component[]; + view: azdata.ModelView; +} + +interface CreateContext { + container: azdata.window.Dialog | azdata.window.Wizard; + onNewValidatorCreated: (validator: Validator) => void; + onNewDisposableCreated: (disposable: vscode.Disposable) => void; + onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent) => void; +} + +export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string }): azdata.InputBoxComponent { + return view.modelBuilder.inputBox().withProperties({ + value: inputInfo.defaultValue, + ariaLabel: inputInfo.ariaLabel, + inputType: 'text', + required: inputInfo.required, + placeHolder: inputInfo.placeHolder, + width: inputInfo.width + }).component(); +} + +export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, fontStyle?: string }): azdata.TextComponent { + const text = view.modelBuilder.text().withProperties({ + value: info.text, + description: info.description, + requiredIndicator: info.required, + CSSStyles: { 'font-style': info.fontStyle || 'normal' } + }).component(); + text.width = info.width; + return text; +} + +export function createNumberInput(view: azdata.ModelView, info: { defaultValue?: string, ariaLabel?: string, min?: number, max?: number, required?: boolean, width?: string, placeHolder?: string }): azdata.InputBoxComponent { + return view.modelBuilder.inputBox().withProperties({ + value: info.defaultValue, + ariaLabel: info.ariaLabel, + inputType: 'number', + min: info.min, + max: info.max, + required: info.required, + width: info.width, + placeHolder: info.placeHolder + }).component(); +} + +export function createCheckbox(view: azdata.ModelView, info: { initialValue: boolean, label: string }): azdata.CheckBoxComponent { + return view.modelBuilder.checkBox().withProperties({ + checked: info.initialValue, + label: info.label + }).component(); +} + +export function createDropdown(view: azdata.ModelView, info: { defaultValue?: string | azdata.CategoryValue, values?: string[] | azdata.CategoryValue[], width?: string }): azdata.DropDownComponent { + return view.modelBuilder.dropDown().withProperties({ + values: info.values, + value: info.defaultValue, + width: info.width + }).component(); +} + +export function initializeDialog(dialogContext: DialogContext): void { + const tabs: azdata.window.DialogTab[] = []; + dialogContext.dialogInfo.tabs.forEach(tabInfo => { + const tab = azdata.window.createTab(tabInfo.title); + tab.registerContent((view: azdata.ModelView) => { + const sections = tabInfo.sections.map(sectionInfo => { + sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputComponentWidth; + sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelComponentWidth; + return createSection({ + sectionInfo: sectionInfo, + view: view, + onNewDisposableCreated: dialogContext.onNewDisposableCreated, + onNewInputComponentCreated: dialogContext.onNewInputComponentCreated, + onNewValidatorCreated: dialogContext.onNewValidatorCreated, + container: dialogContext.container + }); + }); + const formBuilder = view.modelBuilder.formContainer().withFormItems( + sections.map(section => { + return { title: '', component: section }; + }), + { + horizontal: false, + componentWidth: '100%' + } + ); + const form = formBuilder.withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); + tabs.push(tab); + }); + dialogContext.container.content = tabs; +} + +export function initializeWizardPage(context: WizardPageContext): void { + context.page.registerContent((view: azdata.ModelView) => { + const sections = context.sections.map(sectionInfo => { + sectionInfo.inputWidth = sectionInfo.inputWidth || DefaultInputComponentWidth; + sectionInfo.labelWidth = sectionInfo.labelWidth || DefaultLabelComponentWidth; + return createSection({ + view: view, + container: context.container, + onNewDisposableCreated: context.onNewDisposableCreated, + onNewInputComponentCreated: context.onNewInputComponentCreated, + onNewValidatorCreated: context.onNewValidatorCreated, + sectionInfo: sectionInfo + }); + }); + const formBuilder = view.modelBuilder.formContainer().withFormItems( + sections.map(section => { return { title: '', component: section }; }), + { + horizontal: false, + componentWidth: '100%' + } + ); + const form = formBuilder.withLayout({ width: '100%' }).component(); + return view.initializeModel(form); + }); +} + +export function createSection(context: SectionContext): azdata.GroupContainer { + const components: azdata.Component[] = []; + context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputComponentWidth; + context.sectionInfo.labelWidth = context.sectionInfo.labelWidth || DefaultLabelComponentWidth; + if (context.sectionInfo.fields) { + processFields(context.sectionInfo.fields, components, context); + } else if (context.sectionInfo.rows) { + context.sectionInfo.rows.forEach(rowInfo => { + const rowItems: azdata.Component[] = []; + processFields(rowInfo.fields, rowItems, context, context.sectionInfo.spaceBetweenFields || '50px'); + const row = createFlexContainer(context.view, rowItems); + components.push(row); + }); + } + + return createGroupContainer(context.view, components, { + header: context.sectionInfo.title, + collapsible: context.sectionInfo.collapsible === undefined ? true : context.sectionInfo.collapsible, + collapsed: context.sectionInfo.collapsed === undefined ? false : context.sectionInfo.collapsed + }); +} + +function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component[], context: SectionContext, spaceBetweenFields?: string): void { + for (let i = 0; i < fieldInfoArray.length; i++) { + const fieldInfo = fieldInfoArray[i]; + fieldInfo.labelWidth = fieldInfo.labelWidth || context.sectionInfo.labelWidth; + fieldInfo.inputWidth = fieldInfo.inputWidth || context.sectionInfo.inputWidth; + fieldInfo.labelPosition = fieldInfo.labelPosition === undefined ? context.sectionInfo.labelPosition : fieldInfo.labelPosition; + processField({ + view: context.view, + onNewDisposableCreated: context.onNewDisposableCreated, + onNewInputComponentCreated: context.onNewInputComponentCreated, + onNewValidatorCreated: context.onNewValidatorCreated, + fieldInfo: fieldInfo, + container: context.container, + components: components + }); + if (spaceBetweenFields && i < fieldInfoArray.length - 1) { + components.push(context.view.modelBuilder.divContainer().withLayout({ width: spaceBetweenFields }).component()); + } + } +} + +export function createFlexContainer(view: azdata.ModelView, items: azdata.Component[], rowLayout: boolean = true): azdata.FlexContainer { + const flexFlow = rowLayout ? 'row' : 'column'; + const alignItems = rowLayout ? 'center' : ''; + const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px' } } : {}; + return view.modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout({ flexFlow: flexFlow, alignItems: alignItems }).component(); +} + +export function createGroupContainer(view: azdata.ModelView, items: azdata.Component[], layout: azdata.GroupLayout): azdata.GroupContainer { + return view.modelBuilder.groupContainer().withItems(items).withLayout(layout).component(); +} + +function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component, labelPosition?: LabelPosition) { + if (labelPosition && labelPosition === LabelPosition.Left) { + const row = createFlexContainer(view, [label, input]); + components.push(row); + } else { + components.push(label, input); + } +} + + +function processField(context: FieldContext): void { + switch (context.fieldInfo.type) { + case FieldType.Options: + processOptionsTypeField(context); + break; + case FieldType.DateTimeText: + processDateTimeTextField(context); + break; + case FieldType.Number: + processNumberField(context); + break; + case FieldType.SQLPassword: + case FieldType.Password: + processPasswordField(context); + break; + case FieldType.Text: + processTextField(context); + break; + case FieldType.ReadonlyText: + processReadonlyTextField(context); + break; + case FieldType.Checkbox: + processCheckboxField(context); + break; + default: + throw new Error(localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", context.fieldInfo.type)); + } +} + +function processOptionsTypeField(context: FieldContext): void { + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth }); + const dropdown = createDropdown(context.view, { + values: context.fieldInfo.options, + defaultValue: context.fieldInfo.defaultValue, + width: context.fieldInfo.inputWidth + }); + context.onNewInputComponentCreated(context.fieldInfo.variableName!, dropdown); + addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo.labelPosition); +} + +function processDateTimeTextField(context: FieldContext): void { + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const defaultValue = context.fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, ''); // Take the date time information and only leaving the numbers + const input = context.view.modelBuilder.inputBox().withProperties({ + value: defaultValue, + ariaLabel: context.fieldInfo.label, + inputType: 'text', + required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required, + placeHolder: context.fieldInfo.placeHolder + }).component(); + input.width = context.fieldInfo.inputWidth; + context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); + addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition); +} + +function processNumberField(context: FieldContext): void { + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const input = createNumberInput(context.view, { + defaultValue: context.fieldInfo.defaultValue, + ariaLabel: context.fieldInfo.label, + min: context.fieldInfo.min, + max: context.fieldInfo.max, + required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required, + width: context.fieldInfo.inputWidth, + placeHolder: context.fieldInfo.placeHolder + }); + context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); + addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition); +} + +function processTextField(context: FieldContext): void { + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const input = createTextInput(context.view, { + defaultValue: context.fieldInfo.defaultValue, + ariaLabel: context.fieldInfo.label, + required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required, + placeHolder: context.fieldInfo.placeHolder, + width: context.fieldInfo.inputWidth + }); + context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); + addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition); +} + +function processPasswordField(context: FieldContext): void { + const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const passwordInput = context.view.modelBuilder.inputBox().withProperties({ + ariaLabel: context.fieldInfo.label, + inputType: 'password', + required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required, + placeHolder: context.fieldInfo.placeHolder, + width: context.fieldInfo.inputWidth + }).component(); + context.onNewInputComponentCreated(context.fieldInfo.variableName!, passwordInput); + addLabelInputPairToContainer(context.view, context.components, passwordLabel, passwordInput, context.fieldInfo.labelPosition); + + if (context.fieldInfo.type === FieldType.SQLPassword) { + const invalidPasswordMessage = getInvalidSQLPasswordMessage(context.fieldInfo.label); + context.onNewDisposableCreated(passwordInput.onTextChanged(() => { + if (context.fieldInfo.type === FieldType.SQLPassword && isValidSQLPassword(passwordInput.value!, context.fieldInfo.userName)) { + removeValidationMessage(context.container, invalidPasswordMessage); + } + })); + + context.onNewValidatorCreated((): { valid: boolean, message: string } => { + return { valid: isValidSQLPassword(passwordInput.value!, context.fieldInfo.userName), message: invalidPasswordMessage }; + }); + } + + if (context.fieldInfo.confirmationRequired) { + const passwordNotMatchMessage = getPasswordMismatchMessage(context.fieldInfo.label); + const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth }); + const confirmPasswordInput = context.view.modelBuilder.inputBox().withProperties({ + ariaLabel: context.fieldInfo.confirmationLabel, + inputType: 'password', + required: !context.fieldInfo.useCustomValidator, + width: context.fieldInfo.inputWidth + }).component(); + + addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo.labelPosition); + context.onNewValidatorCreated((): { valid: boolean, message: string } => { + const passwordMatches = passwordInput.value === confirmPasswordInput.value; + return { valid: passwordMatches, message: passwordNotMatchMessage }; + }); + + const updatePasswordMismatchMessage = () => { + if (passwordInput.value === confirmPasswordInput.value) { + removeValidationMessage(context.container, passwordNotMatchMessage); + } + }; + + context.onNewDisposableCreated(passwordInput.onTextChanged(() => { + updatePasswordMismatchMessage(); + })); + context.onNewDisposableCreated(confirmPasswordInput.onTextChanged(() => { + updatePasswordMismatchMessage(); + })); + } +} + +function processReadonlyTextField(context: FieldContext): void { + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth }); + const text = createLabel(context.view, { text: context.fieldInfo.defaultValue!, description: '', required: false, width: context.fieldInfo.inputWidth, fontStyle: context.fieldInfo.fontStyle }); + addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo.labelPosition); +} + +function processCheckboxField(context: FieldContext): void { + const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label }); + context.components.push(checkbox); + context.onNewInputComponentCreated(context.fieldInfo.variableName!, checkbox); +} + +export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean { + // Validate SQL Server password + const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase()); + // Instead of using one RegEx, I am seperating it to make it more readable. + const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0; + const hasLowerCase = /[a-z]/.test(password) ? 1 : 0; + const hasNumbers = /\d/.test(password) ? 1 : 0; + const hasNonalphas = /\W/.test(password) ? 1 : 0; + return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonalphas >= 3); +} + +export function removeValidationMessage(container: azdata.window.Dialog | azdata.window.Wizard, message: string): void { + if (container.message && container.message.text.includes(message)) { + const messageWithLineBreak = message + '\n'; + const searchText = container.message.text.includes(messageWithLineBreak) ? messageWithLineBreak : message; + container.message = { text: container.message.text.replace(searchText, '') }; + } +} + +export function getInvalidSQLPasswordMessage(fieldName: string): string { + return localize('invalidSQLPassword', "{0} doesn't meet the password complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy", fieldName); +} + +export function getPasswordMismatchMessage(fieldName: string): string { + return localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldName); +} + +export function setModelValues(inputComponents: InputComponents, model: Model): void { + Object.keys(inputComponents).forEach(key => { + let value; + const input = inputComponents[key]; + if ('checked' in input) { + value = input.checked ? 'true' : 'false'; + } else { + const inputValue = input.value; + if (typeof inputValue === 'string' || typeof inputValue === 'undefined') { + value = inputValue; + } else { + value = inputValue.name; + } + } + + model.setPropertyValue(key, value); + }); +} + +export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean { + return input.value === undefined || input.value === ''; +} + +export const MissingRequiredInformationErrorMessage = localize('deployCluster.MissingRequiredInfoError', "Please fill out the required fields marked with red asterisks."); diff --git a/extensions/resource-deployment/src/ui/notebookInputDialog.ts b/extensions/resource-deployment/src/ui/notebookInputDialog.ts index 3bbf052cb4..f2f16010a7 100644 --- a/extensions/resource-deployment/src/ui/notebookInputDialog.ts +++ b/extensions/resource-deployment/src/ui/notebookInputDialog.ts @@ -2,20 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { DialogBase } from './dialogBase'; import { INotebookService } from '../services/notebookService'; -import { DialogFieldInfo, FieldType, DialogInfo } from '../interfaces'; +import { DialogInfo } from '../interfaces'; +import { Validator, initializeDialog, InputComponents, setModelValues } from './modelViewUtils'; +import { Model } from './model'; +import { EOL } from 'os'; const localize = nls.loadMessageBundle(); export class NotebookInputDialog extends DialogBase { - private variables: { [s: string]: string | undefined; } = {}; - private validators: (() => { valid: boolean, message: string })[] = []; + private inputComponents: InputComponents = {}; constructor(private notebookService: INotebookService, private dialogInfo: DialogInfo) { @@ -24,192 +26,46 @@ export class NotebookInputDialog extends DialogBase { this._dialogObject.okButton.onClick(() => this.onComplete()); } - protected initializeDialog() { - const tabs: azdata.window.DialogTab[] = []; - this.dialogInfo.tabs.forEach(tabInfo => { - const tab = azdata.window.createTab(tabInfo.title); - tab.registerContent((view: azdata.ModelView) => { - const sections: azdata.FormComponentGroup[] = []; - tabInfo.sections.forEach(sectionInfo => { - const fields: azdata.FormComponent[] = []; - sectionInfo.fields.forEach(fieldInfo => { - this.addField(view, fields, fieldInfo); - }); - sections.push({ title: sectionInfo.title, components: fields }); - }); - const formBuilder = view.modelBuilder.formContainer().withFormItems( - sections, - { - horizontal: false - } - ); - - const form = formBuilder.withLayout({ width: '100%' }).component(); - const self = this; - this._dialogObject.registerCloseValidator(() => { - const messages: string[] = []; - self.validators.forEach(validator => { - const result = validator(); - if (!result.valid) { - messages.push(result.message); - } - }); - if (messages.length > 0) { - self._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: messages.join('\n') }; - } else { - self._dialogObject.message = { text: '' }; - } - return messages.length === 0; - }); - - return view.initializeModel(form); - }); - tabs.push(tab); - }); - this._dialogObject.content = tabs; - } - - private addField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void { - this.variables[fieldInfo.variableName] = fieldInfo.defaultValue; - let component: { component: azdata.Component, title: string }[] | azdata.Component | undefined = undefined; - switch (fieldInfo.type) { - case FieldType.Options: - component = this.createOptionsTypeField(view, fieldInfo); - break; - case FieldType.DateTimeText: - component = this.createDateTimeTextField(view, fieldInfo); - break; - case FieldType.Number: - component = this.createNumberField(view, fieldInfo); - break; - case FieldType.SQLPassword: - case FieldType.Password: - component = this.createPasswordField(view, fieldInfo); - break; - case FieldType.Text: - component = this.createTextField(view, fieldInfo); - break; - default: - throw new Error(localize('deploymentDialog.UnknownFieldTypeError', "Unknown field type: \"{0}\"", fieldInfo.type)); - } - - if (component) { - if (Array.isArray(component)) { - fields.push(...component); - } else { - fields.push({ title: fieldInfo.label, component: component }); + protected initialize() { + const self = this; + const validators: Validator[] = []; + initializeDialog({ + dialogInfo: this.dialogInfo, + container: this._dialogObject, + onNewDisposableCreated: (disposable: vscode.Disposable): void => { + this._toDispose.push(disposable); + }, + onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { + this.inputComponents[name] = component; + }, + onNewValidatorCreated: (validator: Validator): void => { + validators.push(validator); } - } else { - throw new Error(localize('deploymentDialog.addFieldError', "Failed to add field: \"{0}\"", fieldInfo.label)); - } - } - - private createOptionsTypeField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.DropDownComponent { - const dropdown = view.modelBuilder.dropDown().withProperties({ values: fieldInfo.options, value: fieldInfo.defaultValue }).component(); - this._toDispose.push(dropdown.onValueChanged(() => { this.variables[fieldInfo.variableName] = dropdown.value; })); - return dropdown; - } - - private createDateTimeTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent { - const defaultValue = fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, ''); - const input = view.modelBuilder.inputBox().withProperties({ - value: defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder - }).component(); - this.variables[fieldInfo.variableName] = defaultValue; - this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; })); - return input; - - } - - private createNumberField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent { - const input = view.modelBuilder.inputBox().withProperties({ - value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'number', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required - }).component(); - this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; })); - return input; - } - - private createTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent { - const input = view.modelBuilder.inputBox().withProperties({ - value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required, placeHolder: fieldInfo.placeHolder - }).component(); - this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; })); - return input; - } - - private createPasswordField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): { title: string, component: azdata.Component }[] { - const components: { title: string, component: azdata.Component }[] = []; - const passwordInput = view.modelBuilder.inputBox().withProperties({ - ariaLabel: fieldInfo.label, inputType: 'password', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder - }).component(); - this._toDispose.push(passwordInput.onTextChanged(() => { this.variables[fieldInfo.variableName] = passwordInput.value; })); - components.push({ title: fieldInfo.label, component: passwordInput }); - - if (fieldInfo.type === FieldType.SQLPassword) { - const invalidPasswordMessage = localize('invalidSQLPassword', "{0} doesn't meet the password complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy", fieldInfo.label); - this._toDispose.push(passwordInput.onTextChanged(() => { - if (fieldInfo.type === FieldType.SQLPassword && this.isValidSQLPassword(fieldInfo, passwordInput)) { - this.removeValidationMessage(invalidPasswordMessage); + }); + this._dialogObject.registerCloseValidator(() => { + const messages: string[] = []; + validators.forEach(validator => { + const result = validator(); + if (!result.valid) { + messages.push(result.message); } - })); - - this.validators.push((): { valid: boolean, message: string } => { - return { valid: this.isValidSQLPassword(fieldInfo, passwordInput), message: invalidPasswordMessage }; }); - } - - if (fieldInfo.confirmationRequired) { - const passwordNotMatchMessage = localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldInfo.label); - - const confirmPasswordInput = view.modelBuilder.inputBox().withProperties({ ariaLabel: fieldInfo.confirmationLabel, inputType: 'password', required: true }).component(); - components.push({ title: fieldInfo.confirmationLabel, component: confirmPasswordInput }); - - this.validators.push((): { valid: boolean, message: string } => { - const passwordMatches = passwordInput.value === confirmPasswordInput.value; - return { valid: passwordMatches, message: passwordNotMatchMessage }; - }); - - const updatePasswordMismatchMessage = () => { - if (passwordInput.value === confirmPasswordInput.value) { - this.removeValidationMessage(passwordNotMatchMessage); - } - }; - - this._toDispose.push(passwordInput.onTextChanged(() => { - updatePasswordMismatchMessage(); - })); - this._toDispose.push(confirmPasswordInput.onTextChanged(() => { - updatePasswordMismatchMessage(); - })); - } - return components; + if (messages.length > 0) { + self._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: messages.join(EOL) }; + } else { + self._dialogObject.message = { text: '' }; + } + return messages.length === 0; + }); } private onComplete(): void { - Object.keys(this.variables).forEach(key => { - process.env[key] = this.variables[key]; + const model: Model = new Model(); + setModelValues(this.inputComponents, model); + model.setEnvironmentVariables(); + this.notebookService.launchNotebook(this.dialogInfo.notebook).then(() => { }, (error) => { + vscode.window.showErrorMessage(error); }); - this.notebookService.launchNotebook(this.dialogInfo.notebook); this.dispose(); } - - private isValidSQLPassword(field: DialogFieldInfo, component: azdata.InputBoxComponent): boolean { - const password = component.value!; - // Validate SQL Server password - const containsUserName = password && field.userName && password.toUpperCase().includes(field.userName.toUpperCase()); - // Instead of using one RegEx, I am seperating it to make it more readable. - const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0; - const hasLowerCase = /[a-z]/.test(password) ? 1 : 0; - const hasNumbers = /\d/.test(password) ? 1 : 0; - const hasNonalphas = /\W/.test(password) ? 1 : 0; - return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonalphas >= 3); - } - - private removeValidationMessage(message: string): void { - if (this._dialogObject.message && this._dialogObject.message.text.includes(message)) { - const messageWithLineBreak = message + '\n'; - const searchText = this._dialogObject.message.text.includes(messageWithLineBreak) ? messageWithLineBreak : message; - this._dialogObject.message = { text: this._dialogObject.message.text.replace(searchText, '') }; - } - } } diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 63a0a98f00..5a64484f01 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -2,19 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { DialogBase } from './dialogBase'; -import { ResourceType, DeploymentProvider } from '../interfaces'; +import { ResourceType, DeploymentProvider, AgreementInfo } from '../interfaces'; import { IResourceTypeService } from '../services/resourceTypeService'; import { IToolsService } from '../services/toolsService'; +import { EOL } from 'os'; +import { createFlexContainer } from './modelViewUtils'; const localize = nls.loadMessageBundle(); export class ResourceTypePickerDialog extends DialogBase { + private toolRefreshTimestamp: number = 0; private _selectedResourceType: ResourceType; private _resourceTypeCards: azdata.CardComponent[] = []; private _view!: azdata.ModelView; @@ -23,6 +25,9 @@ export class ResourceTypePickerDialog extends DialogBase { private _toolsTable!: azdata.TableComponent; private _cardResourceTypeMap: Map = new Map(); private _optionDropDownMap: Map = new Map(); + private _toolsLoadingComponent!: azdata.LoadingComponent; + private _agreementContainer!: azdata.DivContainer; + private _agreementCheckboxChecked: boolean = false; constructor(private extensionContext: vscode.ExtensionContext, private toolsService: IToolsService, @@ -34,8 +39,18 @@ export class ResourceTypePickerDialog extends DialogBase { this._dialogObject.okButton.onClick(() => this.onComplete()); } - initializeDialog() { + initialize() { let tab = azdata.window.createTab(''); + this._dialogObject.registerCloseValidator(() => { + const isValid = this._selectedResourceType && (this._selectedResourceType.agreement === undefined || this._agreementCheckboxChecked); + if (!isValid) { + this._dialogObject.message = { + text: localize('deploymentDialog.AcceptAgreements', "You must agree to the license agreements in order to proceed."), + level: azdata.window.MessageLevel.Error + }; + } + return isValid; + }); tab.registerContent((view: azdata.ModelView) => { const tableWidth = 1126; this._view = view; @@ -43,25 +58,33 @@ export class ResourceTypePickerDialog extends DialogBase { const cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'left' }).component(); this._resourceDescriptionLabel = view.modelBuilder.text().withProperties({ value: this._selectedResourceType ? this._selectedResourceType.description : undefined }).component(); this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - + this._agreementContainer = view.modelBuilder.divContainer().component(); const toolColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolNameColumnHeader', 'Tool'), width: 150 }; const descriptionColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolDescriptionColumnHeader', 'Description'), - width: 850 + width: 650 + }; + const installStatusColumn: azdata.TableColumn = { + value: localize('deploymentDialog.toolStatusColumnHeader', 'Installed'), + width: 100 + }; + const versionColumn: azdata.TableColumn = { + value: localize('deploymentDialog.toolVersionColumnHeader', 'Version'), + width: 100 }; this._toolsTable = view.modelBuilder.table().withProperties({ data: [], - columns: [toolColumn, descriptionColumn], + columns: [toolColumn, descriptionColumn, installStatusColumn, versionColumn], width: tableWidth }).component(); const toolsTableWrapper = view.modelBuilder.divContainer().withLayout({ width: tableWidth }).component(); toolsTableWrapper.addItem(this._toolsTable, { CSSStyles: { 'border-left': '1px solid silver', 'border-top': '1px solid silver' } }); - + this._toolsLoadingComponent = view.modelBuilder.loadingComponent().withItem(toolsTableWrapper).component(); const formBuilder = view.modelBuilder.formContainer().withFormItems( [ { @@ -71,10 +94,14 @@ export class ResourceTypePickerDialog extends DialogBase { component: this._resourceDescriptionLabel, title: '' }, { + component: this._agreementContainer, + title: '' + }, + { component: this._optionsContainer, title: localize('deploymentDialog.OptionsTitle', 'Options') }, { - component: toolsTableWrapper, + component: this._toolsLoadingComponent, title: localize('deploymentDialog.RequiredToolsTitle', 'Required tools') } ], @@ -128,6 +155,12 @@ export class ResourceTypePickerDialog extends DialogBase { } this._resourceDescriptionLabel.value = resourceType.description; + this._agreementCheckboxChecked = false; + this._agreementContainer.clearItems(); + if (resourceType.agreement) { + this._agreementContainer.addItem(this.createAgreementCheckbox(resourceType.agreement)); + } + this._optionsContainer.clearItems(); this._optionDropDownMap.clear(); resourceType.options.forEach(option => { @@ -151,19 +184,67 @@ export class ResourceTypePickerDialog extends DialogBase { } private updateTools(): void { - const tools = this.getCurrentProvider().requiredTools; + this.toolRefreshTimestamp = new Date().getTime(); + const currentRefreshTimestamp = this.toolRefreshTimestamp; + const toolRequirements = this.getCurrentProvider().requiredTools; const headerRowHeight = 28; - this._toolsTable.height = 25 * Math.max(tools.length, 1) + headerRowHeight; - if (tools.length === 0) { + this._toolsTable.height = 25 * Math.max(toolRequirements.length, 1) + headerRowHeight; + if (toolRequirements.length === 0) { + this._dialogObject.okButton.enabled = true; this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']]; } else { - this._toolsTable.data = tools.map(toolRef => { - const tool = this.toolsService.getToolByName(toolRef.name)!; - return [tool.displayName, tool.description]; + const tools = toolRequirements.map(toolReq => { + return this.toolsService.getToolByName(toolReq.name)!; + }); + this._toolsLoadingComponent.loading = true; + this._dialogObject.okButton.enabled = false; + this._dialogObject.message = { + text: '' + }; + + Promise.all(tools.map(tool => tool.loadInformation())).then(() => { + // If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results + if (this.toolRefreshTimestamp !== currentRefreshTimestamp) { + return; + } + const messages: string[] = []; + this._toolsTable.data = toolRequirements.map(toolRef => { + const tool = this.toolsService.getToolByName(toolRef.name)!; + if (!tool.isInstalled) { + messages.push(localize('deploymentDialog.ToolInformation', "{0}: {1}", tool.displayName, tool.homePage)); + if (tool.statusDescription !== undefined) { + console.warn(localize('deploymentDialog.DetailToolStatusDescription', "Additional status information for tool: {0}. {1}", tool.name, tool.statusDescription)); + } + } + return [tool.displayName, tool.description, tool.isInstalled ? localize('deploymentDialog.YesText', "Yes") : localize('deploymentDialog.NoText', "No"), tool.version ? tool.version.version : '']; + }); + this._dialogObject.okButton.enabled = messages.length === 0; + if (messages.length !== 0) { + messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in the debug console.")); + this._dialogObject.message = { + level: azdata.window.MessageLevel.Error, + text: localize('deploymentDialog.ToolCheckFailed', "Some required tools are not installed or do not meet the minimum version requirement."), + description: messages.join(EOL) + }; + } + this._toolsLoadingComponent.loading = false; }); } } + private createAgreementCheckbox(agreementInfo: AgreementInfo): azdata.FlexContainer { + const checkbox = this._view.modelBuilder.checkBox().component(); + checkbox.checked = false; + this._toDispose.push(checkbox.onChanged(() => { + this._agreementCheckboxChecked = checkbox.checked; + })); + const text = this._view.modelBuilder.text().withProperties({ + value: agreementInfo.template, + links: agreementInfo.links + }).component(); + return createFlexContainer(this._view, [checkbox, text]); + } + private getCurrentProvider(): DeploymentProvider { const options: { option: string, value: string }[] = []; diff --git a/extensions/resource-deployment/src/ui/wizardBase.ts b/extensions/resource-deployment/src/ui/wizardBase.ts new file mode 100644 index 0000000000..98b8212ef0 --- /dev/null +++ b/extensions/resource-deployment/src/ui/wizardBase.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vscode-nls'; +import { WizardPageBase } from './wizardPageBase'; +import { Model } from './model'; +const localize = nls.loadMessageBundle(); + +export abstract class WizardBase { + private customButtons: azdata.window.Button[] = []; + private pages: WizardPageBase[] = []; + + public wizardObject: azdata.window.Wizard; + public toDispose: vscode.Disposable[] = []; + public get model(): M { + return this._model; + } + + constructor(private title: string, private _model: M) { + this.wizardObject = azdata.window.createWizard(title); + } + + public open(): Thenable { + this.initialize(); + this.wizardObject.customButtons = this.customButtons; + this.toDispose.push(this.wizardObject.onPageChanged((e) => { + let previousPage = this.pages[e.lastPage]; + let newPage = this.pages[e.newPage]; + previousPage.onLeave(); + newPage.onEnter(); + })); + + this.toDispose.push(this.wizardObject.doneButton.onClick(() => { + this.onOk(); + this.dispose(); + })); + this.toDispose.push(this.wizardObject.cancelButton.onClick(() => { + this.onCancel(); + this.dispose(); + })); + + return this.wizardObject.open().then(() => { + if (this.pages && this.pages.length > 0) { + this.pages[0].onEnter(); + } + }); + + } + + protected abstract initialize(): void; + protected abstract onOk(): void; + protected abstract onCancel(): void; + + public addButton(button: azdata.window.Button) { + this.customButtons.push(button); + } + + protected setPages(pages: WizardPageBase[]) { + this.wizardObject!.pages = pages.map(p => p.pageObject); + this.pages = pages; + this.pages.forEach((page) => { + page.initialize(); + }); + } + + private dispose() { + let errorOccured = false; + this.toDispose.forEach((disposable: vscode.Disposable) => { + try { + disposable.dispose(); + } + catch (error) { + errorOccured = true; + console.error(error); + } + }); + + if (errorOccured) { + vscode.window.showErrorMessage(localize('resourceDeployment.DisposableError', "Error occured while closing the wizard: {0}, open 'Debugger Console' for more information."), this.title); + } + } + + public registerDisposable(disposable: vscode.Disposable): void { + this.toDispose.push(disposable); + } +} diff --git a/extensions/resource-deployment/src/ui/wizardPageBase.ts b/extensions/resource-deployment/src/ui/wizardPageBase.ts new file mode 100644 index 0000000000..09501ea9e8 --- /dev/null +++ b/extensions/resource-deployment/src/ui/wizardPageBase.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Validator } from './modelViewUtils'; + +export abstract class WizardPageBase { + private _page: azdata.window.WizardPage; + private _validators: Validator[] = []; + + constructor(title: string, description: string, private _wizard: T) { + this._page = azdata.window.createWizardPage(title); + this._page.description = description; + } + + public get pageObject(): azdata.window.WizardPage { + return this._page; + } + + public get wizard(): T { + return this._wizard; + } + + public onEnter(): void { } + + public onLeave(): void { } + + public abstract initialize(): void; + + protected get validators(): Validator[] { + return this._validators; + } +} diff --git a/extensions/resource-deployment/yarn.lock b/extensions/resource-deployment/yarn.lock index 8bb371b0d0..be80b527e7 100644 --- a/extensions/resource-deployment/yarn.lock +++ b/extensions/resource-deployment/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@types/yamljs@0.2.30": + version "0.2.30" + resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.30.tgz#d034e1d329e46e8d0f737c9a8db97f68f81b5382" + integrity sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I= + agent-base@4, agent-base@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -19,6 +24,13 @@ ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -227,7 +239,7 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.2: +glob@^7.0.5, glob@^7.1.2: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== @@ -518,6 +530,11 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -628,3 +645,11 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yamljs@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b" + integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ== + dependencies: + argparse "^1.0.7" + glob "^7.0.5" diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 89a44748ed..e0fc74cf3c 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -2745,7 +2745,10 @@ declare module 'azdata' { * Matches the align-content CSS property. */ alignContent?: string; - + /** + * Matches the flex-wrap CSS property. + */ + flexWrap?: string; /** * Container Height */ @@ -2919,11 +2922,11 @@ declare module 'azdata' { * Properties representing the card component, can be used * when using ModelBuilder to create the component */ - export interface CardProperties extends ComponentWithIcon { + export interface CardProperties extends ComponentProperties, ComponentWithIcon { label: string; value?: string; actions?: ActionDescriptor[]; - descriptions?: string[]; + descriptions?: CardDescriptionItem[]; status?: StatusIndicator; /** @@ -2937,6 +2940,13 @@ declare module 'azdata' { cardType?: CardType; } + export interface CardDescriptionItem { + label: string; + value?: string; + tooltip?: string; + fontWeight?: 'normal' | 'bold'; + } + export type InputBoxInputType = 'color' | 'date' | 'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'range' | 'search' | 'text' | 'time' | 'url' | 'week'; export interface ComponentProperties { @@ -3055,6 +3065,8 @@ declare module 'azdata' { export interface TextComponentProperties { value?: string; links?: LinkArea[]; + description?: string; + requiredIndicator?: boolean; CSSStyles?: { [key: string]: string }; } diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index f4414d87fc..afaabe00b1 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -452,7 +452,7 @@ declare module 'sqlops' { label: string; value?: string; actions?: ActionDescriptor[]; - descriptions?: string[]; + descriptions?: CardDescriptionItem[]; status?: StatusIndicator; /** @@ -466,6 +466,11 @@ declare module 'sqlops' { cardType?: CardType; } + export interface CardDescriptionItem { + label: string; + value?: string; + } + export type InputBoxInputType = 'color' | 'date' | 'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'range' | 'search' | 'text' | 'time' | 'url' | 'week'; export interface ComponentProperties { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 0129993ce5..67c4bdc7ab 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -301,12 +301,17 @@ export interface CardProperties { label: string; value?: string; actions?: ActionDescriptor[]; - descriptions?: string[]; + descriptions?: CardDescriptionItem[]; status?: StatusIndicator; selected?: boolean; cardType: CardType; } +export interface CardDescriptionItem { + label: string; + value?: string; +} + export interface ActionDescriptor { label: string; actionTitle?: string; diff --git a/src/sql/workbench/browser/modelComponents/card.component.html b/src/sql/workbench/browser/modelComponents/card.component.html index b166fdfcb7..8095683992 100644 --- a/src/sql/workbench/browser/modelComponents/card.component.html +++ b/src/sql/workbench/browser/modelComponents/card.component.html @@ -1,5 +1,5 @@
+ (mouseout)="onCardHoverChanged($event)" tabIndex="0" [style.width]="width" [style.height]="height">
@@ -13,8 +13,16 @@

{{label}}

-
-
{{desc}}
+
+
+
+
+ {{desc.label}}{{desc.value}} +
+ +
+
+
@@ -50,4 +58,4 @@ - \ No newline at end of file + diff --git a/src/sql/workbench/browser/modelComponents/card.component.ts b/src/sql/workbench/browser/modelComponents/card.component.ts index 22e0c9df31..858aac0116 100644 --- a/src/sql/workbench/browser/modelComponents/card.component.ts +++ b/src/sql/workbench/browser/modelComponents/card.component.ts @@ -15,7 +15,7 @@ import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/theme import { ComponentWithIconBase } from 'sql/workbench/browser/modelComponents/componentWithIconBase'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/browser/modelComponents/interfaces'; -import { StatusIndicator, CardProperties, ActionDescriptor } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { StatusIndicator, CardProperties, ActionDescriptor, CardDescriptionItem } from 'sql/workbench/api/common/sqlExtHostTypes'; @Component({ templateUrl: decodeURI(require.toUrl('./card.component.html')) @@ -145,8 +145,8 @@ export default class CardComponent extends ComponentWithIconBase implements ICom return this.selectable && this.selected; } - public get descriptions(): string[] { - return this.getPropertyOrDefault((props) => props.descriptions, []); + public get descriptions(): CardDescriptionItem[] { + return this.getPropertyOrDefault((props) => props.descriptions, []); } public get actions(): ActionDescriptor[] { diff --git a/src/sql/workbench/browser/modelComponents/divContainer.component.ts b/src/sql/workbench/browser/modelComponents/divContainer.component.ts index e24fb5d489..ab3458ef6f 100644 --- a/src/sql/workbench/browser/modelComponents/divContainer.component.ts +++ b/src/sql/workbench/browser/modelComponents/divContainer.component.ts @@ -23,7 +23,7 @@ class DivItem { @Component({ template: ` -
+
@@ -70,6 +70,7 @@ export default class DivContainer extends ContainerBase im this.updateOverflowY(); } this.updateScroll(); + this.updateClickable(); } private updateOverflowY() { @@ -86,6 +87,17 @@ export default class DivContainer extends ContainerBase im element.dispatchEvent(new Event('scroll')); } + private updateClickable(): void { + const element = this.divContainer.nativeElement; + if (this.clickable) { + element.tabIndex = 0; + element.style.cursor = 'pointer'; + } else { + element.removeAttribute('tabIndex'); + element.style.cursor = 'default'; + } + } + private onClick() { this.fireEvent({ eventType: ComponentEventType.onDidClick, @@ -121,10 +133,6 @@ export default class DivContainer extends ContainerBase im return this.getPropertyOrDefault((props) => props.clickable, false); } - public get tabIndex(): number { - return this.clickable ? 0 : -1; - } - private onKey(e: KeyboardEvent) { let event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { diff --git a/src/sql/workbench/browser/modelComponents/flexContainer.component.ts b/src/sql/workbench/browser/modelComponents/flexContainer.component.ts index 1d83ba62d5..38a4be434d 100644 --- a/src/sql/workbench/browser/modelComponents/flexContainer.component.ts +++ b/src/sql/workbench/browser/modelComponents/flexContainer.component.ts @@ -21,7 +21,7 @@ export class FlexItem { @Component({ template: `
+ [style.alignItems]="alignItems" [style.alignContent]="alignContent" [style.height]="height" [style.width]="width" [style.flex-wrap]="flexWrap">
@@ -40,6 +40,7 @@ export default class FlexContainer extends ContainerBase impleme private _height: string; private _width: string; private _position: string; + private _flexWrap: string; constructor( @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @@ -70,6 +71,7 @@ export default class FlexContainer extends ContainerBase impleme this._position = layout.position ? layout.position : ''; this._height = this.convertSize(layout.height); this._width = this.convertSize(layout.width); + this._flexWrap = layout.flexWrap ? layout.flexWrap : ''; this.layout(); } @@ -107,6 +109,10 @@ export default class FlexContainer extends ContainerBase impleme return this._position; } + public get flexWrap(): string { + return this._flexWrap; + } + private getItemFlex(item: FlexItem): string { return item.config ? item.config.flex : '1 1 auto'; } diff --git a/src/sql/workbench/browser/modelComponents/media/card.css b/src/sql/workbench/browser/modelComponents/media/card.css index 379e524ce8..4dcca366c6 100644 --- a/src/sql/workbench/browser/modelComponents/media/card.css +++ b/src/sql/workbench/browser/modelComponents/media/card.css @@ -15,19 +15,16 @@ vertical-align: top; } -.model-card-list-item.selected, -.model-card.selected { +.model-card-list-item.selected, .model-card.selected { border-color: rgb(0, 120, 215); box-shadow: rgba(0, 120, 215, 0.75) 0px 0px 6px; } -.model-card-list-item.unselected, -.model-card.unselected { +.model-card-list-item.unselected, .model-card.unselected { border-color: rgb(214, 214, 214); box-shadow: none; } - .model-card .card-content { position: relative; display: inline-block; @@ -103,8 +100,7 @@ text-align: center; } -.model-card-list-item .selection-indicator-container, -.model-card .selection-indicator-container { +.model-card-list-item .selection-indicator-container, .model-card .selection-indicator-container { position: absolute; top: 5px; right: 5px; @@ -118,8 +114,7 @@ border-style: solid; } -.model-card-list-item .selection-indicator-container, -.model-card .selection-indicator-container { +.model-card-list-item .selection-indicator-container, .model-card .selection-indicator-container { position: absolute; overflow: hidden; width: 16px; @@ -141,8 +136,7 @@ right: 5px; } -.model-card-list-item .selection-indicator, -.model-card .selection-indicator { +.model-card-list-item .selection-indicator, .model-card .selection-indicator { margin: 4px; width: 8px; height: 8px; @@ -189,12 +183,27 @@ .model-card-list-item .list-item-icon { background-position: 2px 2px; - padding-left:22px; + padding-left: 22px; font-size: 15px; background-repeat: no-repeat; background-size: 16px 16px; } .model-card-list-item .list-item-description { - padding-left:22px; -} \ No newline at end of file + padding-left: 22px; +} + +.model-card-description-container { + border-top-width: 1px; + border-top-style: solid; + border-color: rgb(214, 214, 214); + padding: 5px; +} + +.model-card-list-item-description { + text-align: left; +} + +.model-card-list-item-description-value { + float: right; +} diff --git a/src/sql/workbench/browser/modelComponents/media/info.svg b/src/sql/workbench/browser/modelComponents/media/info.svg new file mode 100644 index 0000000000..6b20c7a149 --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/media/info.svg @@ -0,0 +1 @@ +info_notification \ No newline at end of file diff --git a/src/sql/workbench/browser/modelComponents/media/info_inverse.svg b/src/sql/workbench/browser/modelComponents/media/info_inverse.svg new file mode 100644 index 0000000000..f32f61aa9c --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/media/info_inverse.svg @@ -0,0 +1 @@ +info_notification_inverse \ No newline at end of file diff --git a/src/sql/workbench/browser/modelComponents/media/text.css b/src/sql/workbench/browser/modelComponents/media/text.css new file mode 100644 index 0000000000..23f032ce4b --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/media/text.css @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.modelview-text-tooltip { + background-size: 12px 12px; + background-repeat: no-repeat; + cursor: pointer; + width: 12px; + height: 12px; + margin-left: 5px; +} + +.vs .modelview-text-tooltip { + background-image: url('info.svg') +} + +.vs-dark .modelview-text-tooltip, .hc-black .modelview-text-tooltip { + background-image: url('info_inverse.svg') +} + +.modelview-text-tooltip .modelview-text-tooltip-content { + visibility: hidden; + text-align: left; + border-radius: 5px; + width: 200px; + padding: 5px; + position: absolute; + z-index: 1; + margin-left: 14px; +} + +.vs .modelview-text-tooltip .modelview-text-tooltip-content { + background-color: #f5f5f5; +} + +.vs-dark .modelview-text-tooltip .modelview-text-tooltip-content, .hc-black .modelview-text-tooltip .modelview-text-tooltip-content { + background-color: #050505; +} + +.modelview-text-tooltip:focus .modelview-text-tooltip-content, .modelview-text-tooltip:hover .modelview-text-tooltip-content { + visibility: visible; +} + +.modelview-text-link { + text-decoration: underline !important; +} diff --git a/src/sql/workbench/browser/modelComponents/text.component.ts b/src/sql/workbench/browser/modelComponents/text.component.ts index e7598bea54..42373ae82a 100644 --- a/src/sql/workbench/browser/modelComponents/text.component.ts +++ b/src/sql/workbench/browser/modelComponents/text.component.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/radioButton'; +import 'vs/css!./media/text'; import { Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ElementRef, SecurityContext @@ -18,7 +18,13 @@ import { SafeHtml, DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'modelview-text', template: ` -

` +
+

+

*

+
+
+
+
` }) export default class TextComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { @Input() descriptor: IComponentDescriptor; @@ -57,13 +63,29 @@ export default class TextComponent extends ComponentBase implements IComponent, return this.getPropertyOrDefault((props) => props.value, ''); } + public set description(newValue: string) { + this.setPropertyFromUI((properties, value) => { properties.description = value; }, newValue); + } + + public get description(): string { + return this.getPropertyOrDefault((props) => props.description, ''); + } + + public set requiredIndicator(newValue: boolean) { + this.setPropertyFromUI((properties, value) => { properties.requiredIndicator = value; }, newValue); + } + + public get requiredIndicator(): boolean { + return this.getPropertyOrDefault((props) => props.requiredIndicator, false); + } + public getValue(): SafeHtml { let links = this.getPropertyOrDefault((props) => props.links, []); let text = this._domSanitizer.sanitize(SecurityContext.HTML, this.value); if (links.length !== 0) { for (let i: number = 0; i < links.length; i++) { let link = links[i]; - let linkTag = `${this._domSanitizer.sanitize(SecurityContext.HTML, link.text)}`; + let linkTag = `${this._domSanitizer.sanitize(SecurityContext.HTML, link.text)}`; text = text.replace(`{${i}}`, linkTag); } }