Feature/mssql-big-data-cluster (#4107)

* Adding kubernetes installer.

* Adding variety of kubectl support and integrating into the kubeconfig target cluster page.

* Addressing PR comments, refactored utility file locations and added missing license headers.
This commit is contained in:
Ronald Quan
2019-02-25 15:09:22 -08:00
committed by GitHub
parent a71be2b193
commit d0a4a4242d
19 changed files with 1701 additions and 18 deletions

View File

@@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Shell } from '../utility/shell';
import { Host } from './host';
import { FS } from '../utility/fs';
export interface BinCheckContext {
readonly host: Host;
readonly fs: FS;
readonly shell: Shell;
readonly installDependenciesCallback: () => void;
binFound: boolean;
binPath: string;
}
interface FindBinaryResult {
err: number | null;
output: string;
}
async function findBinary(shell: Shell, binName: string): Promise<FindBinaryResult> {
let cmd = `which ${binName}`;
if (shell.isWindows()) {
cmd = `where.exe ${binName}.exe`;
}
const opts = {
async: true,
env: {
HOME: process.env.HOME,
PATH: process.env.PATH
}
};
const execResult = await shell.execCore(cmd, opts);
if (execResult.code) {
return { err: execResult.code, output: execResult.stderr };
}
return { err: null, output: execResult.stdout };
}
export function execPath(shell: Shell, basePath: string): string {
let bin = basePath;
if (shell.isWindows() && bin && !(bin.endsWith('.exe'))) {
bin = bin + '.exe';
}
return bin;
}
type CheckPresentFailureReason = 'inferFailed' | 'configuredFileMissing';
function alertNoBin(host: Host, binName: string, failureReason: CheckPresentFailureReason, message: string, installDependencies: () => void): void {
switch (failureReason) {
case 'inferFailed':
host.showErrorMessage(message, 'Install dependencies', 'Learn more').then(
(str) => {
switch (str) {
case 'Learn more':
host.showInformationMessage(`Add ${binName} directory to path, or set "mssql-bdc.${binName}-path" config to ${binName} binary.`);
break;
case 'Install dependencies':
installDependencies();
break;
}
}
);
break;
case 'configuredFileMissing':
host.showErrorMessage(message, 'Install dependencies').then(
(str) => {
if (str === 'Install dependencies') {
installDependencies();
}
}
);
break;
}
}
export async function checkForBinary(context: BinCheckContext, bin: string | undefined, binName: string, inferFailedMessage: string, configuredFileMissingMessage: string, alertOnFail: boolean): Promise<boolean> {
if (!bin) {
const fb = await findBinary(context.shell, binName);
if (fb.err || fb.output.length === 0) {
if (alertOnFail) {
alertNoBin(context.host, binName, 'inferFailed', inferFailedMessage, context.installDependenciesCallback);
}
return false;
}
context.binFound = true;
return true;
}
if (context.shell.isWindows) {
context.binFound = context.fs.existsSync(bin);
} else {
const sr = await context.shell.exec(`ls ${bin}`);
context.binFound = (!!sr && sr.code === 0);
}
if (context.binFound) {
context.binPath = bin;
} else {
if (alertOnFail) {
alertNoBin(context.host, binName, 'configuredFileMissing', configuredFileMissingMessage, context.installDependenciesCallback);
}
}
return context.binFound;
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Errorable, failed } from '../interfaces';
interface CompatibilityGuaranteed {
readonly guaranteed: true;
}
interface CompatibilityNotGuaranteed {
readonly guaranteed: false;
readonly didCheck: boolean;
readonly clientVersion: string;
readonly serverVersion: string;
}
export type Compatibility = CompatibilityGuaranteed | CompatibilityNotGuaranteed;
export function isGuaranteedCompatible(c: Compatibility): c is CompatibilityGuaranteed {
return c.guaranteed;
}
export interface Version {
readonly major: string;
readonly minor: string;
readonly gitVersion: string;
}
export async function check(kubectlLoadJSON: (cmd: string) => Promise<Errorable<any>>): Promise<Compatibility> {
const version = await kubectlLoadJSON('version -o json');
if (failed(version)) {
return {
guaranteed: false,
didCheck: false,
clientVersion: '',
serverVersion: ''
};
}
const clientVersion: Version = version.result.clientVersion;
const serverVersion: Version = version.result.serverVersion;
if (isCompatible(clientVersion, serverVersion)) {
return { guaranteed: true };
}
return {
guaranteed: false,
didCheck: true,
clientVersion: clientVersion.gitVersion,
serverVersion: serverVersion.gitVersion
};
}
function isCompatible(clientVersion: Version, serverVersion: Version): boolean {
if (clientVersion.major === serverVersion.major) {
const clientMinor = Number.parseInt(clientVersion.minor);
const serverMinor = Number.parseInt(serverVersion.minor);
if (Number.isInteger(clientMinor) && Number.isInteger(serverMinor) && Math.abs(clientMinor - serverMinor) <= 1) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface Host {
showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined>;
showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined>;
showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined>;
getConfiguration(key: string): any;
onDidChangeConfiguration(listener: (ch: vscode.ConfigurationChangeEvent) => any): vscode.Disposable;
}
export const host: Host = {
showErrorMessage : showErrorMessage,
showWarningMessage : showWarningMessage,
showInformationMessage : showInformationMessage,
getConfiguration : getConfiguration,
onDidChangeConfiguration : onDidChangeConfiguration,
};
function showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(message, ...items);
}
function showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showWarningMessage(message, ...items);
}
function showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showInformationMessage(message, ...items);
}
function getConfiguration(key: string): any {
return vscode.workspace.getConfiguration(key);
}
function onDidChangeConfiguration(listener: (e: vscode.ConfigurationChangeEvent) => any): vscode.Disposable {
return vscode.workspace.onDidChangeConfiguration(listener);
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from "vscode";
export interface ISqlServerBigDataClusterChannel {
showOutput(message: any, title?: string): void;
}
class SqlServerBigDataCluster implements ISqlServerBigDataClusterChannel {
private readonly channel: vscode.OutputChannel = vscode.window.createOutputChannel("SQL Server big data cluster");
showOutput(message: any, title?: string): void {
if (title) {
const simplifiedTime = (new Date()).toISOString().replace(/z|t/gi, ' ').trim(); // YYYY-MM-DD HH:mm:ss.sss
const hightlightingTitle = `[${title} ${simplifiedTime}]`;
this.channel.appendLine(hightlightingTitle);
}
this.channel.appendLine(message);
this.channel.show();
}
}
export const sqlserverbigdataclusterchannel: ISqlServerBigDataClusterChannel = new SqlServerBigDataCluster();

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Host } from './host';
import { FS } from '../utility/fs';
import { Shell, ShellResult } from '../utility/shell';
import * as binutil from './binutil';
import { Errorable } from '../interfaces';
import * as compatibility from './compatibility';
import { getToolPath } from '../config/config';
export interface Kubectl {
checkPresent(errorMessageMode: CheckPresentMessageMode): Promise<boolean>;
asJson<T>(command: string): Promise<Errorable<T>>;
}
interface Context {
readonly host: Host;
readonly fs: FS;
readonly shell: Shell;
readonly installDependenciesCallback: () => void;
binFound: boolean;
binPath: string;
}
class KubectlImpl implements Kubectl {
constructor(host: Host, fs: FS, shell: Shell, installDependenciesCallback: () => void, kubectlFound: boolean) {
this.context = { host : host, fs : fs, shell : shell, installDependenciesCallback : installDependenciesCallback, binFound : kubectlFound, binPath : 'kubectl' };
}
private readonly context: Context;
checkPresent(errorMessageMode: CheckPresentMessageMode): Promise<boolean> {
return checkPresent(this.context, errorMessageMode);
}
asJson<T>(command: string): Promise<Errorable<T>> {
return asJson(this.context, command);
}
}
export function create(host: Host, fs: FS, shell: Shell, installDependenciesCallback: () => void): Kubectl {
return new KubectlImpl(host, fs, shell, installDependenciesCallback, false);
}
export enum CheckPresentMessageMode {
Command,
Activation,
Silent,
}
async function checkPresent(context: Context, errorMessageMode: CheckPresentMessageMode): Promise<boolean> {
if (context.binFound) {
return true;
}
return await checkForKubectlInternal(context, errorMessageMode);
}
async function checkForKubectlInternal(context: Context, errorMessageMode: CheckPresentMessageMode): Promise<boolean> {
const binName = 'kubectl';
const bin = getToolPath(context.host, context.shell, binName);
const contextMessage = getCheckKubectlContextMessage(errorMessageMode);
const inferFailedMessage = `Could not find "${binName}" binary.${contextMessage}`;
const configuredFileMissingMessage = `${bin} is not installed. ${contextMessage}`;
return await binutil.checkForBinary(context, bin, binName, inferFailedMessage, configuredFileMissingMessage, errorMessageMode !== CheckPresentMessageMode.Silent);
}
function getCheckKubectlContextMessage(errorMessageMode: CheckPresentMessageMode): string {
if (errorMessageMode === CheckPresentMessageMode.Activation) {
return ' SQL Server Big data cluster requires kubernetes.';
} else if (errorMessageMode === CheckPresentMessageMode.Command) {
return ' Cannot execute command.';
}
return '';
}
async function invokeAsync(context: Context, command: string, stdin?: string): Promise<ShellResult | undefined> {
if (await checkPresent(context, CheckPresentMessageMode.Command)) {
const bin = baseKubectlPath(context);
const cmd = `${bin} ${command}`;
const sr = await context.shell.exec(cmd, stdin);
if (sr && sr.code !== 0) {
checkPossibleIncompatibility(context);
}
return sr;
} else {
return { code: -1, stdout: '', stderr: '' };
}
}
// TODO: invalidate this when the context changes or if we know kubectl has changed (e.g. config)
let checkedCompatibility = false; // We don't want to spam the user (or CPU!) repeatedly running the version check
async function checkPossibleIncompatibility(context: Context): Promise<void> {
if (checkedCompatibility) {
return;
}
checkedCompatibility = true;
const compat = await compatibility.check((cmd) => asJson<compatibility.Version>(context, cmd));
if (!compatibility.isGuaranteedCompatible(compat) && compat.didCheck) {
const versionAlert = `kubectl version ${compat.clientVersion} may be incompatible with cluster Kubernetes version ${compat.serverVersion}`;
context.host.showWarningMessage(versionAlert);
}
}
function baseKubectlPath(context: Context): string {
let bin = getToolPath(context.host, context.shell, 'kubectl');
if (!bin) {
bin = 'kubectl';
}
return bin;
}
async function asJson<T>(context: Context, command: string): Promise<Errorable<T>> {
const shellResult = await invokeAsync(context, command);
if (!shellResult) {
return { succeeded: false, error: [`Unable to run command (${command})`] };
}
if (shellResult.code === 0) {
return { succeeded: true, result: JSON.parse(shellResult.stdout.trim()) as T };
}
return { succeeded: false, error: [ shellResult.stderr ] };
}

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from "vscode";
import { Kubectl } from "./kubectl";
import { failed } from "../interfaces";
export interface KubectlContext {
readonly clusterName: string;
readonly contextName: string;
readonly userName: string;
readonly active: boolean;
}
interface Kubeconfig {
readonly apiVersion: string;
readonly 'current-context': string;
readonly clusters: {
readonly name: string;
readonly cluster: {
readonly server: string;
readonly 'certificate-authority'?: string;
readonly 'certificate-authority-data'?: string;
};
}[] | undefined;
readonly contexts: {
readonly name: string;
readonly context: {
readonly cluster: string;
readonly user: string;
readonly namespace?: string;
};
}[] | undefined;
readonly users: {
readonly name: string;
readonly user: {};
}[] | undefined;
}
export interface ClusterConfig {
readonly server: string;
readonly certificateAuthority: string | undefined;
}
async function getKubeconfig(kubectl: Kubectl): Promise<Kubeconfig | null> {
const shellResult = await kubectl.asJson<any>("config view -o json");
if (failed(shellResult)) {
vscode.window.showErrorMessage(shellResult.error[0]);
return null;
}
return shellResult.result;
}
export async function getCurrentClusterConfig(kubectl: Kubectl): Promise<ClusterConfig | undefined> {
const kubeConfig = await getKubeconfig(kubectl);
if (!kubeConfig || !kubeConfig.clusters || !kubeConfig.contexts) {
return undefined;
}
const contextConfig = kubeConfig.contexts.find((context) => context.name === kubeConfig["current-context"])!;
const clusterConfig = kubeConfig.clusters.find((cluster) => cluster.name === contextConfig.context.cluster)!;
return {
server: clusterConfig.cluster.server,
certificateAuthority: clusterConfig.cluster["certificate-authority"]
};
}
export async function getContexts(kubectl: Kubectl): Promise<KubectlContext[]> {
const kubectlConfig = await getKubeconfig(kubectl);
if (!kubectlConfig) {
return [];
}
const currentContext = kubectlConfig["current-context"];
const contexts = kubectlConfig.contexts || [];
return contexts.map((c) => {
return {
clusterName: c.context.cluster,
contextName: c.name,
userName: c.context.user,
active: c.name === currentContext
};
});
}