Ron/bdc script (#4221)

* WIP adding scripting support.

* Adding deploy command along with additional env vars needed.

* Adding script generation that sets envars, kube context, and mssqlctl

* Adding test email for docker email envar until we update UI.

* Adding cluster platform detection and disabling generate script after first click.

* Fix spacing and adding comment.
This commit is contained in:
Ronald Quan
2019-02-28 14:26:50 -08:00
committed by GitHub
parent 70d86ce9a2
commit 0d1ebce1a1
8 changed files with 212 additions and 11 deletions

View File

@@ -69,3 +69,11 @@ export enum ToolInstallationStatus {
NotInstalled,
Installing
}
export enum ClusterType {
Unknown = 0,
AKS,
Minikube,
Kubernetes,
Other
}

View File

@@ -14,6 +14,8 @@ import { getToolPath } from '../config/config';
export interface Kubectl {
checkPresent(errorMessageMode: CheckPresentMessageMode): Promise<boolean>;
asJson<T>(command: string): Promise<Errorable<T>>;
invokeAsync(command: string, stdin?: string): Promise<ShellResult | undefined>;
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<boolean> {
return checkPresent(this.context, errorMessageMode);
@@ -38,6 +40,13 @@ class KubectlImpl implements Kubectl {
asJson<T>(command: string): Promise<Errorable<T>> {
return asJson(this.context, command);
}
invokeAsync(command: string, stdin?: string): Promise<ShellResult | undefined> {
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<void> {
}
function baseKubectlPath(context: Context): string {
export function baseKubectlPath(context: Context): string {
let bin = getToolPath(context.host, context.shell, 'kubectl');
if (!bin) {
bin = 'kubectl';

View File

@@ -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<KubectlContext[]> {
active: c.name === currentContext
};
});
}
export async function setContext(kubectl: Kubectl, targetContext: string): Promise<void> {
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<ClusterType> {
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;
}

View File

@@ -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';

View File

@@ -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<ScriptingDictionary<string>>;
getTargetKubectlContext() : KubectlContext;
}
export interface ScriptingDictionary<V> {
[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<void> {
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);
}
};

View File

@@ -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<string> = {};
constructor(private _kubectl : Kubectl) {
}
public async loadClusters(): Promise<KubectlContext[]> {
return await getContexts(this._kubectl);
}
public async changeKubernetesContext(targetContext: string): Promise<void> {
await setContext(this._kubectl, targetContext)
}
public getDefaultPorts(): Thenable<ClusterPorts> {
let promise = new Promise<ClusterPorts>(resolve => {
resolve({
@@ -135,4 +141,56 @@ export class CreateClusterModel {
public containerRegistryUserName: string;
public containerRegistryPassword: string;
public async getTargetClusterPlatform(targetContextName : string) : Promise<string> {
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<ScriptingDictionary<string>> {
// 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;
}
}

View File

@@ -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<CreateClusterModel, CreateClusterWizard> {
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<CreateClusterModel, CreateCl
this.setPages([targetClusterTypePage, settingsPage, clusterProfilePage, selectTargetClusterPage, summaryPage]);
this.wizardObject.generateScriptButton.label = localize('bdc-create.generateScriptsButtonText', 'Generate Scripts');
this.wizardObject.generateScriptButton.hidden = true;
this.wizardObject.generateScriptButton.hidden = false;
this.wizardObject.doneButton.label = localize('bdc-create.createClusterButtonText', 'Create');
this.wizardObject.generateScriptButton.onClick(() => { });
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(() => { });
}
}