diff --git a/extensions/big-data-cluster/src/interfaces.ts b/extensions/big-data-cluster/src/interfaces.ts index 250a2f5de6..e02bb245b7 100644 --- a/extensions/big-data-cluster/src/interfaces.ts +++ b/extensions/big-data-cluster/src/interfaces.ts @@ -69,3 +69,11 @@ export enum ToolInstallationStatus { NotInstalled, Installing } + +export enum ClusterType { + Unknown = 0, + AKS, + Minikube, + Kubernetes, + Other +} \ No newline at end of file diff --git a/extensions/big-data-cluster/src/kubectl/kubeChannel.ts b/extensions/big-data-cluster/src/kubectl/SqlServerBigDataClusterChannel.ts similarity index 100% rename from extensions/big-data-cluster/src/kubectl/kubeChannel.ts rename to extensions/big-data-cluster/src/kubectl/SqlServerBigDataClusterChannel.ts diff --git a/extensions/big-data-cluster/src/kubectl/kubectl.ts b/extensions/big-data-cluster/src/kubectl/kubectl.ts index 78b52f9f21..819764eaff 100644 --- a/extensions/big-data-cluster/src/kubectl/kubectl.ts +++ b/extensions/big-data-cluster/src/kubectl/kubectl.ts @@ -14,6 +14,8 @@ import { getToolPath } from '../config/config'; export interface Kubectl { checkPresent(errorMessageMode: CheckPresentMessageMode): Promise; asJson(command: string): Promise>; + invokeAsync(command: string, stdin?: string): Promise; + getContext(): Context; } interface Context { @@ -30,7 +32,7 @@ class KubectlImpl implements Kubectl { this.context = { host : host, fs : fs, shell : shell, installDependenciesCallback : installDependenciesCallback, binFound : kubectlFound, binPath : 'kubectl' }; } - private readonly context: Context; + readonly context: Context; checkPresent(errorMessageMode: CheckPresentMessageMode): Promise { return checkPresent(this.context, errorMessageMode); @@ -38,6 +40,13 @@ class KubectlImpl implements Kubectl { asJson(command: string): Promise> { return asJson(this.context, command); } + invokeAsync(command: string, stdin?: string): Promise { + return invokeAsync(this.context, command, stdin); + } + + getContext(): Context { + return this.context; + } } export function create(host: Host, fs: FS, shell: Shell, installDependenciesCallback: () => void): Kubectl { @@ -108,7 +117,7 @@ async function checkPossibleIncompatibility(context: Context): Promise { } -function baseKubectlPath(context: Context): string { +export function baseKubectlPath(context: Context): string { let bin = getToolPath(context.host, context.shell, 'kubectl'); if (!bin) { bin = 'kubectl'; diff --git a/extensions/big-data-cluster/src/kubectl/kubectlUtils.ts b/extensions/big-data-cluster/src/kubectl/kubectlUtils.ts index ba43a9a253..737e39c1e8 100644 --- a/extensions/big-data-cluster/src/kubectl/kubectlUtils.ts +++ b/extensions/big-data-cluster/src/kubectl/kubectlUtils.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode"; import { Kubectl } from "./kubectl"; -import { failed } from "../interfaces"; +import { failed, ClusterType } from "../interfaces"; export interface KubectlContext { readonly clusterName: string; @@ -83,4 +83,50 @@ export async function getContexts(kubectl: Kubectl): Promise { active: c.name === currentContext }; }); +} + +export async function setContext(kubectl: Kubectl, targetContext: string): Promise { + const shellResult = await kubectl.invokeAsync(`config use-context ${targetContext}`); + if (!shellResult || shellResult.code != 0) { + // TODO: Update error handling for now. + vscode.window.showErrorMessage(`Failed to set '${targetContext}' as current cluster: ${shellResult ? shellResult.stderr : "Unable to run kubectl"}`); + } +} + +export async function inferCurrentClusterType(kubectl: Kubectl): Promise { + let latestContextName = ""; + + const ctxsr = await kubectl.invokeAsync('config current-context'); + if (ctxsr && ctxsr.code === 0) { + latestContextName = ctxsr.stdout.trim(); + } else { + return ClusterType.Other; + } + + const cisr = await kubectl.invokeAsync('cluster-info'); + if (!cisr || cisr.code !== 0) { + return ClusterType.Unknown; + } + const masterInfos = cisr.stdout.split('\n') + .filter((s) => s.indexOf('master is running at') >= 0); + + if (masterInfos.length === 0) { + return ClusterType.Other; + } + + const masterInfo = masterInfos[0]; + if (masterInfo.indexOf('azmk8s.io') >= 0 || masterInfo.indexOf('azure.com') >= 0) { + return ClusterType.AKS; + } + + if (latestContextName) { + const gcsr = await kubectl.invokeAsync(`config get-contexts ${latestContextName}`); + if (gcsr && gcsr.code === 0) { + if (gcsr.stdout.indexOf('minikube') >= 0) { + return ClusterType.Minikube; + } + } + } + + return ClusterType.Other; } \ No newline at end of file diff --git a/extensions/big-data-cluster/src/main.ts b/extensions/big-data-cluster/src/main.ts index 6eeb297f72..b8fb635124 100644 --- a/extensions/big-data-cluster/src/main.ts +++ b/extensions/big-data-cluster/src/main.ts @@ -10,7 +10,7 @@ import { MainController } from './mainController'; import { fs } from './utility/fs'; import { host } from './kubectl/host'; -import { sqlserverbigdataclusterchannel } from './kubectl/kubeChannel'; +import { sqlserverbigdataclusterchannel } from './kubectl/SqlServerBigDataClusterChannel'; import { shell, Shell } from './utility/shell'; import { CheckPresentMessageMode, create as kubectlCreate } from './kubectl/kubectl'; import { installKubectl } from './installer/installer'; diff --git a/extensions/big-data-cluster/src/scripting/scripting.ts b/extensions/big-data-cluster/src/scripting/scripting.ts new file mode 100644 index 0000000000..cec3aac18d --- /dev/null +++ b/extensions/big-data-cluster/src/scripting/scripting.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { fs } from '../utility/fs'; +import { Shell } from '../utility/shell'; +import * as vscode from 'vscode'; +import * as path from 'path'; +import mkdirp = require('mkdirp'); +import { Kubectl, baseKubectlPath } from '../kubectl/kubectl'; +import { KubectlContext } from '../kubectl/kubectlUtils'; + + export interface Scriptable { + getScriptProperties(): Promise>; + getTargetKubectlContext() : KubectlContext; + } + + export interface ScriptingDictionary { + [name: string]: V; +} + +const deployFilePrefix : string = 'mssql-bdc-deploy'; +export class ScriptGenerator { + + private _shell: Shell; + private _kubectl: Kubectl; + + private _kubectlPath: string; + constructor(_kubectl: Kubectl) { + this._kubectl = _kubectl; + this._shell = this._kubectl.getContext().shell; + this._kubectlPath = baseKubectlPath(this._kubectl.getContext()); + } + + public async generateDeploymentScript(scriptable: Scriptable) : Promise { + let targetClusterName = scriptable.getTargetKubectlContext().clusterName; + let targetContextName = scriptable.getTargetKubectlContext().contextName; + + let timestamp = new Date().getTime(); + let deployFolder = this.getDeploymentFolder(this._shell); + let deployFileSuffix = this._shell.isWindows() ? `.bat` : `.sh`; + let deployFileName = `${deployFilePrefix}-${targetClusterName}-${timestamp}${deployFileSuffix}`; + let deployFilePath = path.join(deployFolder, deployFileName); + + let envVars = ""; + let propertiesDict = await scriptable.getScriptProperties(); + for (let key in propertiesDict) { + let value = propertiesDict[key]; + envVars += this._shell.isWindows() ? `Set ${key} = ${value}\n` : `export ${key} = ${value}\n`; + } + envVars += '\n'; + + let kubeContextcommand = `${this._kubectlPath} config use-context ${targetContextName}\n`; + // Todo: The API for mssqlctl may change per version, so need a version check to use proper syntax. + let deployCommand = `mssqlctl create cluster ${targetClusterName}\n`; + + let deployContent = envVars + kubeContextcommand + deployCommand; + + mkdirp.sync(deployFolder); + await fs.writeFile(deployFilePath, deployContent, handleError); + } + + public getDeploymentFolder(shell: Shell): string { + return path.join(shell.home(), `.mssql-bdc/deployment`); + } +} + +const handleError = (err: NodeJS.ErrnoException) => { + if (err) { + vscode.window.showErrorMessage(err.message); + } +}; \ No newline at end of file diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts index cdf68ee1bc..485ff5cfea 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts @@ -5,23 +5,29 @@ 'use strict'; import { TargetClusterType, ClusterPorts, ContainerRegistryInfo, TargetClusterTypeInfo, ToolInfo, ToolInstallationStatus } from '../../interfaces'; -import { getContexts, KubectlContext } from '../../kubectl/kubectlUtils'; +import { getContexts, KubectlContext, setContext, inferCurrentClusterType } from '../../kubectl/kubectlUtils'; import { Kubectl } from '../../kubectl/kubectl'; +import { Scriptable, ScriptingDictionary } from '../../scripting/scripting'; +import { ClusterType} from '../../interfaces'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -export class CreateClusterModel { +export class CreateClusterModel implements Scriptable { private _tmp_tools_installed: boolean = false; - - constructor(private _kubectl: Kubectl) { + private scriptingProperties : ScriptingDictionary = {}; + constructor(private _kubectl : Kubectl) { } public async loadClusters(): Promise { return await getContexts(this._kubectl); } + public async changeKubernetesContext(targetContext: string): Promise { + await setContext(this._kubectl, targetContext) + } + public getDefaultPorts(): Thenable { let promise = new Promise(resolve => { resolve({ @@ -135,4 +141,56 @@ export class CreateClusterModel { public containerRegistryUserName: string; public containerRegistryPassword: string; + + public async getTargetClusterPlatform(targetContextName : string) : Promise { + await setContext(this._kubectl, targetContextName); + let clusterType = await inferCurrentClusterType(this._kubectl); + + switch (clusterType) { + case ClusterType.AKS: + return 'aks'; + case ClusterType.Minikube: + return 'minikube'; + case ClusterType.Other: + default: + return 'kubernetes'; + } + } + + public async getScriptProperties() : Promise> { + + // Cluster settings + this.scriptingProperties['CLUSTER_NAME'] = this.selectedCluster.clusterName; + this.scriptingProperties['CLUSTER_PLATFORM'] = await this.getTargetClusterPlatform(this.selectedCluster.contextName); + + // Default pool count for now. TODO: Update from user input + this.scriptingProperties['CLUSTER_DATA_POOL_REPLICAS'] = '1'; + this.scriptingProperties['CLUSTER_COMPUTE_POOL_REPLICAS'] = '2'; + this.scriptingProperties['CLUSTER_STORAGE_POOL_REPLICAS'] = '3'; + + // SQL Server settings + this.scriptingProperties['CONTROLLER_USERNAME'] = this.adminUserName; + this.scriptingProperties['CONTROLLER_PASSWORD'] = this.adminPassword; + this.scriptingProperties['KNOX_PASSWORD'] = this.adminPassword; + this.scriptingProperties['MSSQL_SA_PASSWORD'] = this.adminPassword; + + // docker settings + this.scriptingProperties['DOCKER_REPOSITORY'] = this.containerRepository; + this.scriptingProperties['DOCKER_REGISTRY' ] = this.containerRegistry; + this.scriptingProperties['DOCKER_PASSWORD'] = this.containerRegistryPassword; + this.scriptingProperties['DOCKER_USERNAME'] = this.containerRegistryUserName; + this.scriptingProperties['DOCKER_IMAGE_TAG'] = this.containerImageTag; + + // port settings + this.scriptingProperties['MASTER_SQL_PORT'] = this.sqlPort; + this.scriptingProperties['KNOX_PORT'] = this.knoxPort; + this.scriptingProperties['GRAFANA_PORT'] = this.grafanaPort; + this.scriptingProperties['KIBANA_PORT'] = this.kibanaPort; + + return this.scriptingProperties; + } + + public getTargetKubectlContext() : KubectlContext { + return this.selectedCluster; + } } diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts index 189ef6a70a..847d9db455 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts @@ -14,13 +14,15 @@ import { WizardBase } from '../wizardBase'; import * as nls from 'vscode-nls'; import { Kubectl } from '../../kubectl/kubectl'; import { SelectTargetClusterTypePage } from './pages/selectTargetClusterTypePage'; - +import { ScriptGenerator } from '../../scripting/scripting'; const localize = nls.loadMessageBundle(); export class CreateClusterWizard extends WizardBase { + private scripter : ScriptGenerator; constructor(context: ExtensionContext, kubectl: Kubectl) { let model = new CreateClusterModel(kubectl); super(model, context, localize('bdc-create.wizardTitle', 'Create a big data cluster')); + this.scripter = new ScriptGenerator(kubectl); } protected initialize(): void { @@ -32,10 +34,16 @@ export class CreateClusterWizard extends WizardBase { }); + this.wizardObject.generateScriptButton.onClick(async () => { + this.wizardObject.generateScriptButton.enabled = false; + this.scripter.generateDeploymentScript(this.model).then( () => { + this.wizardObject.generateScriptButton.enabled = true; + //TODO: Add error handling. + }); + }); this.wizardObject.doneButton.onClick(() => { }); } }