From 1921c7adc583a25a6b250cc7040601ee302336bc Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 25 Aug 2023 08:54:10 -0400 Subject: [PATCH] Refactor code that interacts with `odo` - Split `oc.ts` up, so that commands that interact with odo go into odoWrapper.ts and commands that interact with oc go into `ocWrapper.ts` - Remove most methods from `./src/odo/command.ts`, and instead create the `CommandText` objects directly in the corresponding method in `odoWrapper.ts` - Adapt the integration tests to the change, and add a few more integration tests - Delete some dead code Closes #3078 Signed-off-by: David Thompson --- build/bundle-tools.ts | 1 - package-lock.json | 47 +- src/explorer.ts | 50 +- src/extension.ts | 2 +- src/k8s/common.ts | 37 +- src/k8s/console.ts | 109 ++-- src/k8s/csv.ts | 42 +- src/k8s/deployment.ts | 33 -- src/k8s/route.ts | 26 +- src/oc/ocWrapper.ts | 367 +++++++++++++ src/oc/types.ts | 14 + src/odo.ts | 391 -------------- src/odo/command.ts | 222 -------- src/odo/componentType.ts | 1 + src/odo/odoTypes.ts | 17 + src/odo/odoWrapper.ts | 499 ++++++++++++++++++ src/odo/workspace.ts | 6 +- src/odo3.ts | 144 ----- src/openshift/cluster.ts | 306 ++++++----- src/openshift/component.ts | 27 +- src/openshift/openshiftItem.ts | 26 +- src/openshift/project.ts | 29 +- src/registriesView.ts | 25 +- src/serverlessFunction/commands.ts | 20 +- src/serverlessFunction/functions.ts | 31 +- src/serverlessFunction/manageRepository.ts | 10 +- src/util/loginUtil.ts | 31 ++ src/util/swagger.ts | 4 +- .../addServiceBindingViewLoader.ts | 4 +- .../create-component/createComponentLoader.ts | 14 +- .../devfile-registry/registryViewLoader.ts | 4 +- .../serverlessFunctionLoader.ts | 4 +- src/{oc.ts => yamlFileCommands.ts} | 21 +- test/integration/command.test.ts | 312 +---------- test/integration/helm.test.ts | 12 +- test/integration/loginUtils.test.ts | 36 ++ test/integration/ocWrapper.test.ts | 246 +++++++++ test/integration/odo.test.ts | 315 ----------- test/integration/odo3.test.ts | 93 ---- test/integration/odoWrapper.test.ts | 359 +++++++++++++ test/integration/project.test.ts | 75 --- test/unit/debug.test.ts | 9 +- test/unit/explorer.test.ts | 4 +- test/unit/extension.test.ts | 17 +- test/unit/index.ts | 1 + test/unit/oc.test.ts | 38 +- test/unit/odo/workspace.test.ts | 15 +- test/unit/{odo.test.ts => odoWrapper.test.ts} | 28 +- test/unit/openshift/cluster.test.ts | 120 ++--- test/unit/openshift/component.test.ts | 178 +------ test/unit/openshift/nameValidator.test.ts | 4 +- test/unit/openshift/project.test.ts | 17 +- test/unit/openshift/testOSItem.ts | 62 --- 53 files changed, 2117 insertions(+), 2388 deletions(-) delete mode 100644 src/k8s/deployment.ts create mode 100644 src/oc/ocWrapper.ts create mode 100644 src/oc/types.ts delete mode 100644 src/odo.ts create mode 100644 src/odo/odoTypes.ts create mode 100644 src/odo/odoWrapper.ts delete mode 100644 src/odo3.ts create mode 100644 src/util/loginUtil.ts rename src/{oc.ts => yamlFileCommands.ts} (75%) create mode 100644 test/integration/loginUtils.test.ts create mode 100644 test/integration/ocWrapper.test.ts delete mode 100644 test/integration/odo.test.ts delete mode 100644 test/integration/odo3.test.ts create mode 100644 test/integration/odoWrapper.test.ts delete mode 100644 test/integration/project.test.ts rename test/unit/{odo.test.ts => odoWrapper.test.ts} (89%) delete mode 100644 test/unit/openshift/testOSItem.ts diff --git a/build/bundle-tools.ts b/build/bundle-tools.ts index 9f6ed592a..ebfe20088 100644 --- a/build/bundle-tools.ts +++ b/build/bundle-tools.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ /* eslint-disable guard-for-in */ -/* eslint-disable no-restricted-syntax */ /* eslint-disable no-console */ import * as fs from 'fs'; diff --git a/package-lock.json b/package-lock.json index 73a8aa0bf..32f7b2623 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8524,6 +8524,20 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -18199,21 +18213,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -23418,6 +23417,16 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } } }, "css-color-keywords": { @@ -30211,14 +30220,6 @@ "webidl-conversions": "^3.0.0" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, "which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", diff --git a/src/explorer.ts b/src/explorer.ts index 411f8a2a9..500ae8e64 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -7,20 +7,23 @@ import { Context, KubernetesObject } from '@kubernetes/client-node'; import * as fs from 'fs'; import * as path from 'path'; import { - commands, Disposable, + Disposable, Event, - EventEmitter, extensions, ThemeIcon, + EventEmitter, + ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, - Uri, version, + Uri, + commands, + extensions, + version, window } from 'vscode'; -import { CliChannel } from './cli'; import * as Helm from './helm/helm'; -import { Command } from './odo/command'; -import { newInstance, Odo3 } from './odo3'; +import { Oc } from './oc/ocWrapper'; +import { Odo } from './odo/odoWrapper'; import { KubeConfigUtils } from './util/kubeUtils'; import { Platform } from './util/platform'; import { Progress } from './util/progress'; @@ -59,8 +62,6 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos readonly onDidChangeTreeData: Event = this .eventEmitter.event; - private odo3: Odo3 = newInstance(); - private constructor() { try { this.kubeConfig = new KubeConfigUtils(); @@ -90,17 +91,6 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos }); } - async getCurrentClusterUrl(): Promise { - // print odo version and Server URL if user is logged in - const result = await CliChannel.getInstance().executeTool(Command.printOdoVersion()); - // search for line with 'Server:' substring - const clusterLine = result.stdout.trim().split('\n').find((value) => value.includes('Server:')); - // if line with Server: is printed out it means user is logged in - void commands.executeCommand('setContext', 'isLoggedIn', !!clusterLine); - // cut out server url after 'Server:' substring - return clusterLine ? clusterLine.substring(clusterLine.indexOf(':') + 1).trim() : undefined; - } - static getInstance(): OpenShiftExplorer { if (!OpenShiftExplorer.instance) { OpenShiftExplorer.instance = new OpenShiftExplorer(); @@ -182,7 +172,7 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos let result: ExplorerItem[] = []; if (!element) { try { - await this.odo3.getNamespaces() + await Odo.Instance.getProjects(); result = [this.kubeContext]; if (this.kubeContext) { const homeDir = this.kubeConfig.findHomeDir(); @@ -205,9 +195,9 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos // * example is sandbox context created when login to sandbox first time // (3) there is namespace set in context and namespace exists in the cluster // (4) there is namespace set in context and namespace does not exist in the cluster - const namespaces = await this.odo3.getNamespaces(); + const namespaces = await Odo.Instance.getProjects(); if (this.kubeContext.namespace) { - if (namespaces.find(item => item?.metadata.name === this.kubeContext.namespace)) { + if (namespaces.find(item => item.name === this.kubeContext.namespace)) { result = [{ kind: 'project', metadata: { @@ -216,14 +206,14 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos } as KubernetesObject] } else if (namespaces.length >= 1) { // switch to first accessible namespace - await this.odo3.setNamespace(namespaces[0].metadata.name); + await Odo.Instance.setProject(namespaces[0].name); } else { result = [CREATE_OR_SET_PROJECT_ITEM] } } else { // get list of projects or namespaces // find default namespace - if (namespaces.find(item => item?.metadata.name === 'default')) { + if (namespaces.find(item => item?.name === 'default')) { result = [{ kind: 'project', metadata: { @@ -235,14 +225,14 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos } } } else { - result = [...await this.odo3.getDeploymentConfigs(), ...await this.odo3.getDeployments(), ...await Helm.getHelmReleases()]; + result = [ + ...await Oc.Instance.getKubernetesObjects('DeploymentConfig'), + ...await Oc.Instance.getKubernetesObjects('Deployment'), + ...await Helm.getHelmReleases() + ]; } // don't show Open In Developer Dashboard if not openshift cluster - const openshiftResources = await CliChannel.getInstance().executeTool(Command.isOpenshiftCluster(), undefined, false); - let isOpenshiftCluster = true; - if (openshiftResources.stdout.length === 0){ - isOpenshiftCluster = false; - } + const isOpenshiftCluster = await Oc.Instance.isOpenShiftCluster(); void commands.executeCommand('setContext', 'isOpenshiftCluster', isOpenshiftCluster); if (!element) { diff --git a/src/extension.ts b/src/extension.ts index c375cf577..959fa3027 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -79,7 +79,7 @@ export async function activate(extensionContext: ExtensionContext): Promise(command: string): Promise { - const kubectl = await k8s.extension.kubectl.v1; - if (kubectl.available) { - const result = await kubectl.api.invokeCommand(`${command} -o json`); - return JSON.parse(result.stdout) as T; - } - return; -} - -export async function execKubectl(command: string) { - const kubectl = await k8s.extension.kubectl.v1; - if (kubectl.available) { - const result = await kubectl.api.invokeCommand(`${command}`); - return result; - } - throw new Error('Cannot find kubectl command.'); -} - -export function loadItems(json: string, fetch: (data) => I[] = (data): I[] => data.items): I[] { - let data: I[] = []; - try { - const items = fetch(JSON.parse(json)); - if (items) data = items; - } catch (ignore) { - // ignore parse errors and return empty array - } - return data; -} diff --git a/src/k8s/console.ts b/src/k8s/console.ts index ce1a0b406..fbc1ca4b9 100644 --- a/src/k8s/console.ts +++ b/src/k8s/console.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import { CliChannel } from '../cli'; -import { Command } from '../odo/command'; +import { Oc } from '../oc/ocWrapper'; +import { ClusterType } from '../oc/types'; import { KubeConfigUtils } from '../util/kubeUtils'; import { vsCommand } from '../vscommand'; @@ -17,84 +18,100 @@ export class Console { return project; } - static async fetchOpenshiftConsoleUrl(): Promise { - try { - return await Console.cli.executeTool(Command.showConsoleUrl()); - } catch (ignore) { - const consoleUrl = await Console.cli.executeTool(Command.showServerUrl()); - return consoleUrl.stdout; - } - } - - static openShift4ClusterUrl(consoleUrl: any): string { - return JSON.parse(consoleUrl.stdout).data.consoleURL; - } - @vsCommand('clusters.openshift.build.openConsole') - static async openBuildConfig(context: { name: string}): Promise { - let url = ''; + static async openBuildConfig(context: { name: string}): Promise { if (!context) { void vscode.window.showErrorMessage('Cannot load the build config'); return; } - const consoleUrl = await Console.fetchOpenshiftConsoleUrl(); + const consoleInfo = await Oc.Instance.getConsoleInfo(); const project = Console.getCurrentProject(); - if (consoleUrl.stdout) { - url = `${Console.openShift4ClusterUrl(consoleUrl)}/k8s/ns/${project}/buildconfigs/${context.name}`; - } else { - url = `${consoleUrl}/console/project/${project}/browse/builds/${context.name}?tab=history`; + + let url: string = undefined; + switch (consoleInfo.kind) { + case ClusterType.Kubernetes: { + url = `${consoleInfo.url}/project/${project}/browse/builds/${context.name}?tab=history`; + break; + } + case ClusterType.OpenShift: { + url = `${consoleInfo.url}/k8s/ns/${project}/buildconfigs/${context.name}`; + break; + } + default: + throw new Error('Should be impossible, since the cluster must be either OpenShift or non-OpenShift'); } - return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); } @vsCommand('clusters.openshift.deployment.openConsole') - static async openDeploymentConfig(context: { name: string}): Promise { - let url = ''; + static async openDeploymentConfig(context: { name: string}): Promise { if (!context) { void vscode.window.showErrorMessage('Cannot load the deployment config'); return; } const project = Console.getCurrentProject(); - const consoleUrl = await Console.fetchOpenshiftConsoleUrl(); - if (consoleUrl.stdout) { - url = `${Console.openShift4ClusterUrl(consoleUrl)}/k8s/ns/${project}/deploymentconfigs/${context.name}`; - } else { - url = `${consoleUrl}/console/project/${project}/browse/dc/${context.name}?tab=history`; + const consoleInfo = await Oc.Instance.getConsoleInfo(); + let url = ''; + switch (consoleInfo.kind) { + case ClusterType.Kubernetes: { + url = `${consoleInfo.url}/project/${project}/browse/dc/${context.name}?tab=history`; + break; + } + case ClusterType.OpenShift: { + url = `${consoleInfo.url}/k8s/ns/${project}/deploymentconfigs/${context.name}`; + break; + } + default: + throw new Error('Should be impossible, since the cluster must be either OpenShift or non-OpenShift'); } - return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); } @vsCommand('clusters.openshift.imagestream.openConsole') - static async openImageStream(context: { name: string}): Promise { - let url = ''; + static async openImageStream(context: { name: string}): Promise { if (!context) { void vscode.window.showErrorMessage('Cannot load the image stream'); return; } const project = Console.getCurrentProject(); - const consoleUrl = await Console.fetchOpenshiftConsoleUrl(); - if (consoleUrl.stdout) { - url = `${Console.openShift4ClusterUrl(consoleUrl)}/k8s/ns/${project}/imagestreams/${context.name}`; - } else { - url = `${consoleUrl}/console/project/${project}/browse/images/${context.name}`; + const consoleInfo = await Oc.Instance.getConsoleInfo(); + let url = ''; + switch (consoleInfo.kind) { + case ClusterType.Kubernetes: { + url = `${consoleInfo.url}/project/${project}/browse/images/${context.name}`; + break; + } + case ClusterType.OpenShift: { + url = `${consoleInfo.url}/k8s/ns/${project}/imagestreams/${context.name}`; + break; + } + default: + throw new Error('Should be impossible, since the cluster must be either OpenShift or non-OpenShift'); } - return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); } @vsCommand('clusters.openshift.project.openConsole') - static async openProject(context: { name: string}): Promise { - let url = ''; + static async openProject(context: { name: string}): Promise { if (!context) { void vscode.window.showErrorMessage('Cannot load the Project'); return; } const project = Console.getCurrentProject(); - const consoleUrl = await Console.fetchOpenshiftConsoleUrl(); - if (consoleUrl.stdout) { - url = `${Console.openShift4ClusterUrl(consoleUrl)}/k8s/cluster/projects/${project}`; - } else { - url = `${consoleUrl}/console/project/${project}/overview`; + const consoleInfo = await Oc.Instance.getConsoleInfo(); + let url = ''; + switch (consoleInfo.kind) { + case ClusterType.Kubernetes: { + url = `${consoleInfo.url}/project/${project}/overview`; + break; + } + case ClusterType.OpenShift: { + url = `${consoleInfo.url}/k8s/cluster/projects/${project}`; + break; + } + default: + throw new Error('Should be impossible, since the cluster must be either OpenShift or non-OpenShift'); } - return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); } } diff --git a/src/k8s/csv.ts b/src/k8s/csv.ts index e1d0e6263..5c111d02e 100644 --- a/src/k8s/csv.ts +++ b/src/k8s/csv.ts @@ -8,14 +8,13 @@ import { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; import { TreeItem, WebviewPanel, window } from 'vscode'; import { ClusterExplorerV1 } from 'vscode-kubernetes-tools-api'; -import { getInstance } from '../odo'; -import { Command } from '../odo/command'; +import { Oc } from '../oc/ocWrapper'; +import { Odo } from '../odo/odoWrapper'; import OpenShiftItem from '../openshift/openshiftItem'; import { getOpenAPISchemaFor } from '../util/swagger'; -import { vsCommand, VsCommandError } from '../vscommand'; +import { vsCommand } from '../vscommand'; import CreateServiceViewLoader from '../webview/create-service/createServiceViewLoader'; -import * as common from './common'; -import { ClusterServiceVersionKind, CRDDescription, CustomResourceDefinitionKind } from './olm/types'; +import { CRDDescription, ClusterServiceVersionKind, CustomResourceDefinitionKind } from './olm/types'; import { DEFAULT_K8S_SCHEMA, generateDefaults, getUISchema, randomString } from './utils'; class CsvNode implements ClusterExplorerV1.Node, ClusterExplorerV1.ClusterExplorerExtensionNode { @@ -62,8 +61,7 @@ export class ClusterServiceVersion extends OpenShiftItem { parent?.resourceKind?.manifestKind === 'ClusterServiceVersion'; }, async getChildren(parent: ClusterExplorerV1.ClusterExplorerNode | undefined): Promise { - const getCsvCmd = ClusterServiceVersion.command.getCsv((parent as any).name); - const csv: ClusterServiceVersionKind = await common.asJson(getCsvCmd); + const csv: ClusterServiceVersionKind = (await Oc.Instance.getKubernetesObject('csv', (parent as any).name)) as unknown as ClusterServiceVersionKind; return csv.spec.customresourcedefinitions.owned.map((crd) => new CsvNode(crd, csv)); }, }; @@ -83,14 +81,19 @@ export class ClusterServiceVersion extends OpenShiftItem { if (event.command === 'create') { // add waiting for Deployment to be created using wait --for=condition // no need to wait until it is available - if (!await getInstance().getActiveCluster()) { + if (!await Odo.Instance.getActiveCluster()) { // could be expired session return; } try { - await OpenShiftItem.odo.createService(event.formData); - void window.showInformationMessage(`Service ${event.formData.metadata.name} successfully created.` ); + // make the service part of the app + event.formData.metadata.labels = { + app: 'app', + 'app.kubernetes.io/part-of': 'app' + }; + await Oc.Instance.createKubernetesObjectFromSpec(event.formData); + void window.showInformationMessage(`Service ${event.formData.metadata.name} successfully created.`); panel.dispose(); } catch (err) { void window.showErrorMessage(err); @@ -105,26 +108,11 @@ export class ClusterServiceVersion extends OpenShiftItem { return ClusterServiceVersion.createNewServiceFromDescriptor(crdOwnedNode.impl.crdDescription, crdOwnedNode.impl.csv); } - static async getAuthToken(): Promise { - const gcuCmd = Command.getCurrentUserName(); - const gcuExecRes = await this.odo.execute(gcuCmd, undefined, false); - if (gcuExecRes.error) { - throw new VsCommandError(gcuExecRes.stderr, `Cannot get current user name. '${gcuCmd}' returned non zero error code.`); - } - const gcutCmd = Command.getCurrentUserToken(); - const gcutExecRes = await this.odo.execute(gcutCmd, undefined, false); - if (gcutExecRes.error) { - throw new VsCommandError(gcuExecRes.stderr, `Cannot get current user name. '${gcutCmd}' returned non zero error code.`); - } - return gcutExecRes.stdout.trim(); - } - static async createNewServiceFromDescriptor(crdDescription: CRDDescription, csv: ClusterServiceVersionKind): Promise { - const getCrdCmd = ClusterServiceVersion.command.getCrd(crdDescription.name); let crdResource: CustomResourceDefinitionKind; let apiVersion: string; try { - crdResource = await common.asJson(getCrdCmd); + crdResource = await Oc.Instance.getKubernetesObject('crd', crdDescription.name) as unknown as CustomResourceDefinitionKind; } catch (err) { // if crd cannot be accessed, try to use swagger } @@ -135,7 +123,7 @@ export class ClusterServiceVersion extends OpenShiftItem { apiVersion = `${crdResource.spec.group}/${crdDescription.version}`; } else { const activeCluster = await this.odo.getActiveCluster(); - const token = await this.getAuthToken(); + const token = await Oc.Instance.getCurrentUserToken(); openAPIV3SchemaAll = await getOpenAPISchemaFor(activeCluster, token, crdDescription.kind, crdDescription.version); const gvk = _.find(openAPIV3SchemaAll['x-kubernetes-group-version-kind'], ({ group, version, kind }) => crdDescription.version === version && crdDescription.kind === kind && group); diff --git a/src/k8s/deployment.ts b/src/k8s/deployment.ts deleted file mode 100644 index 6397d2519..000000000 --- a/src/k8s/deployment.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import { CommandOption, CommandText } from '../base/command'; - -export const Command = { - delete(name: string): CommandText { - // oc delete all -l "app.kubernetes.io/component=nodejs-basic" --cascade=true - // it deletes everything related to component created as an example in dev console - // except webhooks secrets declared in deployment descriptor. Not all the hooks - // exist at the moment of deletion. In addition tools should get the list of - // the hooks and try to delete them without failing if some of them are not - // present in namespace - // TODO: Parse the deployment description, get the hooks and delete them - // ignoring the possible errors - return new CommandText('oc', - 'delete all', [ - new CommandOption('-l', `app.kubernetes.io/component=${name}`, true, true), - new CommandOption('--wait=true'), - new CommandOption('--cascade=true') - ] - ); - }, - get(): CommandText { - return new CommandText('oc', - 'get deployments', [ - new CommandOption('-o', 'json') - ] - ); - } -} diff --git a/src/k8s/route.ts b/src/k8s/route.ts index d7cdd6d80..ba45123a4 100644 --- a/src/k8s/route.ts +++ b/src/k8s/route.ts @@ -5,25 +5,18 @@ import { KubeConfig } from '@kubernetes/client-node'; import { commands, Uri } from 'vscode'; +import { Oc } from '../oc/ocWrapper'; import { vsCommand, VsCommandError } from '../vscommand'; -import { asJson } from './common'; - export class Route { +export class Route { - public static command = { - getRoute(namespace: string, name: string): string { - return `get route ${name} -n ${namespace}`; + public static async getUrl(namespace: string, name: string): Promise { + const route = await Oc.Instance.getKubernetesObject('route', name, namespace); + const hostName = (route as any)?.spec.host; + if (hostName === undefined) { + throw new VsCommandError(`Cannot identify host name for Route '${name}'`, 'Cannot identify host name for Route'); } - }; - - public static getUrl(namespace: string, name: string): Promise { - return asJson(Route.command.getRoute(namespace, name)).then((response: any) => { - const hostName = response?.spec.host; - if (hostName === undefined) { - throw new VsCommandError(`Cannot identify host name for Route '${name}'`, 'Cannot identify host name for Route'); - } - return response.spec.tls ? `https://${hostName}` : `http://${hostName}`; - }); + return (route as any).spec.tls ? `https://${hostName}` : `http://${hostName}`; } @vsCommand('clusters.openshift.route.open') @@ -37,4 +30,5 @@ import { asJson } from './common'; const url = await Route.getUrl(currentContextObj.namespace, context.name) await commands.executeCommand('vscode.open', Uri.parse(url)); } - } + +} diff --git a/src/oc/ocWrapper.ts b/src/oc/ocWrapper.ts new file mode 100644 index 000000000..c8a5d6717 --- /dev/null +++ b/src/oc/ocWrapper.ts @@ -0,0 +1,367 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { KubernetesObject } from '@kubernetes/client-node/dist/types'; +import * as fs from 'fs/promises'; +import * as tmp from 'tmp'; +import { CommandOption, CommandText } from '../base/command'; +import { CliChannel } from '../cli'; +import { ClusterType, KubernetesConsole } from './types'; + +/** + * A wrapper around the `oc` CLI tool. + * + * This class is a stateless singleton. + * This makes it easier to stub its methods than if it were a bunch of directly exported functions. + */ +export class Oc { + private static INSTANCE = new Oc(); + + static get Instance() { + return Oc.INSTANCE; + } + + /** + * Returns a list of all resources of the given type in the given namespace. + * + * If no namespace is supplied, the current namespace is used. + * + * @param resourceType the type of resource to get a list of + * @param namespace the namespace to list the resources of (defaults to the current namespace if none is provided) + * @returns a list of all resources of the given type in the given namespace + */ + public async getKubernetesObjects( + resourceType: string, + namespace?: string, + ): Promise { + const result = await CliChannel.getInstance().executeTool( + Oc.getKubernetesObjectCommand(resourceType, namespace), + ); + return JSON.parse(result.stdout).items; + } + + /** + * Returns the Kubernetes resource with the given name and type in the given namespace. + * + * If no namespace is supplied, the current namespace is used. + * + * @param resourceType the type of resource to get + * @param resourceType the name of the resource to get + * @param namespace the namespace to list the resources of (defaults to the current namespace if none is provided) + * @returns the Kubernetes resource with the given name and type in the given namespace + */ + public async getKubernetesObject( + resourceType: string, + resourceName: string, + namespace?: string, + ): Promise { + const result = await CliChannel.getInstance().executeTool( + Oc.getSingleKubernetesObjectCommand(resourceType, resourceName, namespace), + ); + return JSON.parse(result.stdout); + } + + public async getAllKubernetesObjects(namespace?: string): Promise { + const result = await CliChannel.getInstance().executeTool( + Oc.getKubernetesObjectCommand('all', namespace), + ); + return JSON.parse(result.stdout).items; + } + + /** + * Delete the given Kubernetes resource + * + * @param resourceType the type of the Kubernetes resource to delete + * @param resourceName the name of the Kubernetes resource to delete + * @param namespace the namespace that the Kubernetes resource to delete is in. If not provided, the current namespace will be used + */ + public async deleteKubernetesObject( + resourceType: string, + resourceName: string, + namespace?: string, + ) { + await CliChannel.getInstance().executeTool( + Oc.deleteKubernetesObjectCommand(resourceType, resourceName, namespace), + ); + } + + /** + * Create a Kubernetes object from the given spec. + * + * @param spec the spec of the kubernetes object to create + */ + public async createKubernetesObjectFromSpec(spec: object) { + const jsonString = JSON.stringify(spec); + const tempJsonFile = await new Promise((resolve, reject) => { + tmp.file({ postfix: '.json' }, (err, name) => { + if (err) { + reject(err); + } + resolve(name); + }); + }); + try { + await fs.writeFile(tempJsonFile, jsonString); + // call oc create -f path/to/file until odo does support creating services without component + await CliChannel.getInstance().executeTool( + new CommandText('oc', 'create', [new CommandOption('-f', tempJsonFile)]), + ); + } finally { + await fs.unlink(tempJsonFile); + } + } + + /** + * Create a Kubernetes object from the given file. + * + * @param file the file containing the spec of the kubernetes object to create + */ + public async createKubernetesObjectFromFile(file: string) { + await CliChannel.getInstance().executeTool( + new CommandText('oc', 'create', [new CommandOption('-f', file)]), + ); + } + + /** + * Returns the username of the current user. + * + * @returns the username of the current user + */ + public async getCurrentUser(): Promise { + const result = await CliChannel.getInstance().executeTool(new CommandText('oc', 'whoami')); + return result.stdout; + } + + /** + * Returns the token for the current user. + * + * @returns the token for the current user + * @throws if no user is logged in + */ + public async getCurrentUserToken(): Promise { + const result = await CliChannel.getInstance().executeTool(new CommandText('oc', 'whoami -t')); + return result.stdout.trim(); + } + + /** + * Returns true if the current user is authorized to create a pod in the current namespace on the cluster, and false otherwise. + * + * @returns true if the current user is authorized to create a pod in the current namespace on the cluster, and false otherwise + */ + public async canCreatePod(): Promise { + try { + const result = await CliChannel.getInstance().executeTool( + new CommandText('oc', 'auth can-i create pod'), + ); + if (result.stdout === 'yes') { + return true; + } + } catch { + //ignore + } + return false; + } + + /** + * Deletes all deployments in the current namespace that have a label "component" with a value `componentName`. + * + * @param componentName the value of the component label to match + */ + public async deleteDeploymentByComponentLabel(componentName: string): Promise { + await CliChannel.getInstance().executeTool(new CommandText('oc', 'delete deployment', [ + new CommandOption('-l', `component='${componentName}'`), + ])); + } + + /** + * Returns the url and type of the console for the current cluster. + * + * @returns the url and type of the console for the current cluster + */ + public async getConsoleInfo(): Promise { + try { + // only works on OpenShift + const getUrlObj = await CliChannel.getInstance().executeTool( + new CommandText( + 'oc', 'get configmaps console-public -n openshift-config-managed -o json', + ), + ); + const consoleUrl = JSON.parse(getUrlObj.stdout).data.consoleURL; + return { + kind: ClusterType.OpenShift, + url: consoleUrl, + }; + } catch (ignore) { + const serverUrl = await CliChannel.getInstance().executeTool( + new CommandText('oc', 'whoami --show-server'), + ); + const consoleUrl = `${serverUrl.stdout}/console`; + return { + kind: ClusterType.Kubernetes, + url: consoleUrl, + } + } + } + + /** + * Log into the given OpenShift cluster using the given username and password. + * + * @param clusterURL the URL of the cluster to log in to + * @param username the username to use when logging in + * @param password the password to use when logging in + */ + public async loginWithUsernamePassword( + clusterURL: string, + username: string, + password: string, + ): Promise { + const result = await CliChannel.getInstance().executeTool( + new CommandText('oc', `login ${clusterURL}`, [ + new CommandOption('-u', username, true, true), + new CommandOption('-p', password, true, true), + new CommandOption('--insecure-skip-tls-verify'), + ]), + ); + if (result.stderr) { + throw new Error(result.stderr); + } + } + + /** + * Log into the given OpenShift cluster using the provided token. + * + * @param clusterURL the URL of the cluster to log in to + * @param token the token to use to log in to the cluster + */ + public async loginWithToken(clusterURL: string, token: string): Promise { + const result = await CliChannel.getInstance().executeTool( + new CommandText('oc', `login ${clusterURL}`, [ + new CommandOption('--token', token.trim()), + new CommandOption('--insecure-skip-tls-verify'), + ]), + ); + if (result.stderr) { + throw new Error(result.stderr); + } + } + + /** + * Log out of the current OpenShift cluster. + * + * @throws if you are not currently logged into an OpenShift cluster + */ + public async logout(): Promise { + await CliChannel.getInstance().executeTool(new CommandText('oc', 'logout')); + } + + /** + * Switches the current Kubernetes context to the given named context. + * + * @param contextName the name of the context to switch to + */ + public async setContext(contextName: string): Promise { + await CliChannel.getInstance().executeTool( + new CommandText('oc', `config use-context ${contextName}`), + ); + } + + /** + * Deletes the given Kubernetes context from the kube config. + * + * @param contextName the name of the context to delete + */ + public async deleteContext(contextName: string): Promise { + await CliChannel.getInstance().executeTool( + new CommandText('oc', `config delete-context ${contextName}`), + ); + } + + /** + * Deletes the given user from the kube config. + * + * @param username the user to delete + */ + public async deleteUser(username: string): Promise { + await CliChannel.getInstance().executeTool(new CommandText('oc', `config delete-user ${username}`)); + } + + /** + * Deletes the given cluster from the kube config. + * + * @param cluster the name of the cluster to delete + */ + public async deleteCluster(cluster: string): Promise { + await CliChannel.getInstance().executeTool(new CommandText('oc', `config delete-cluster ${cluster}`)); + } + + /** + * Returns true if the current cluster is an OpenShift cluster, and false otherwise + * + * @returns true if the current cluster is an OpenShift cluster, and false otherwise + */ + public async isOpenShiftCluster(): Promise { + try { + const result = await CliChannel.getInstance().executeTool(new CommandText('oc api-resources | grep openshift')); + if (result.stdout.length === 0) { + return false; + } + return true; + } catch (e) { + // probably no + return false; + } + } + + /** + * Returns the oc command to list all resources of the given type in the given (or current) namespace + * + * @param resourceType the resource type to get + * @param namespace the namespace from which to get all the stateful sets + * @returns the oc command to list all resources of the given type in the given (or current) namespace + */ + private static getKubernetesObjectCommand( + resourceType: string, + namespace?: string, + ): CommandText { + if (!resourceType) { + throw new Error('Must pass the resource type to get'); + } + const args = [new CommandOption('-o', 'json')]; + if (namespace) { + args.push(new CommandOption('--namespace', namespace)); + } + return new CommandText('oc', `get ${resourceType}`, args); + } + + private static getSingleKubernetesObjectCommand( + resourceType: string, + resourceName: string, + namespace?: string, + ): CommandText { + if (!resourceType) { + throw new Error('Must pass the resource type to get'); + } + const args = [new CommandOption('-o', 'json')]; + if (namespace) { + args.push(new CommandOption('--namespace', namespace)); + } + return new CommandText('oc', `get ${resourceType} ${resourceName}`, args); + } + + private static deleteKubernetesObjectCommand( + resourceType: string, + resourceName: string, + namespace?: string, + ): CommandText { + if (!resourceType || !resourceName) { + throw new Error('Must pass the resource type and resource name'); + } + const args = []; + if (namespace) { + args.push(new CommandOption('--namespace', namespace)); + } + return new CommandText('oc', `delete ${resourceType} ${resourceName}`, args); + } +} diff --git a/src/oc/types.ts b/src/oc/types.ts new file mode 100644 index 000000000..ae15f00b0 --- /dev/null +++ b/src/oc/types.ts @@ -0,0 +1,14 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +export enum ClusterType { + OpenShift, + Kubernetes +}; + +export type KubernetesConsole = { + kind: ClusterType, + url: string, +}; diff --git a/src/odo.ts b/src/odo.ts deleted file mode 100644 index 787f7e53f..000000000 --- a/src/odo.ts +++ /dev/null @@ -1,391 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -/* eslint-disable @typescript-eslint/no-use-before-define */ -/* eslint-disable class-methods-use-this */ -/* eslint-disable @typescript-eslint/no-misused-promises */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable no-await-in-loop */ - -import { KubeConfig } from '@kubernetes/client-node'; -import * as fs from 'fs'; -import { pathExistsSync } from 'fs-extra'; -import * as path from 'path'; -import * as tempfile from 'tmp'; -import { commands, ProviderResult, QuickPickItem, Uri, workspace, WorkspaceFolder } from 'vscode'; -import { CommandText } from './base/command'; -import * as cliInstance from './cli'; -import { Command } from './odo/command'; -import { AnalyzeResponse, ComponentType, ComponentTypeAdapter, DevfileComponentType, Registry } from './odo/componentType'; -import { ComponentDescription } from './odo/componentTypeDescription'; -import { Project } from './odo/project'; -import { ToolsConfig } from './tools'; -import { ChildProcessUtil, CliExitData } from './util/childProcessUtil'; -import { KubeConfigUtils } from './util/kubeUtils'; -import { Platform } from './util/platform'; -import { VsCommandError } from './vscommand'; - -export enum ContextType { - CLUSTER = 'cluster', - PROJECT = 'project', - APPLICATION = 'application', - COMPONENT = 'componentNotPushed', - COMPONENT_OTHER = 'componentOther', - COMPONENT_PUSHED = 'component', - COMPONENT_NO_CONTEXT = 'componentNoContext', - SERVICE = 'service', - CLUSTER_DOWN = 'clusterDown', - LOGIN_REQUIRED = 'loginRequired', -} - -export interface OpenShiftObject extends QuickPickItem { - getChildren(): ProviderResult; - removeChild(item: OpenShiftObject): Promise; - addChild(item: OpenShiftObject): Promise; - getParent(): OpenShiftObject; - getName(): string; - contextValue: ContextType; - compType?: string; - contextPath?: Uri; - path?: string; - iconPath?: Uri; -} - -export interface Odo { - getProjects(): Promise; - getCompTypesJson():Promise; - getComponentTypes(): Promise; - execute(command: CommandText, cwd?: string, fail?: boolean, addEnv?: any): Promise; - requireLogin(): Promise; - createProject(name: string): Promise; - deleteProject(projectName: string): Promise; - createComponentFromFolder(type: string, registryName: string, name: string, path: Uri, starterName?: string, useExistingDevfile?: boolean, customDevfilePath?: string): Promise; - createService(formData: any): Promise; - loadItems(result: CliExitData, fetch: (data) => I[]): I[]; - getRegistries(): Promise; - addRegistry(name: string, url: string, token: string): Promise; - removeRegistry(name: string): Promise; - describeComponent(contextPath: string, experimental?: boolean): Promise; - analyze(contextPath: string): Promise; - canCreatePod(): Promise; - isPodmanPresent(): Promise; - - /** - * Returns the URL of the API of the current active cluster, - * or undefined if there are no active clusters. - * - * @return the URL of the API of the current active cluster, - * or undefined if there are no active clusters - */ - getActiveCluster(): Promise; - - /** - * Returns the active project or null if no project is active - * - * @returns the active project or null if no project is active - */ - getActiveProject(): Promise; - - /** - * Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path. - * - * @param componentPath the path to the component - */ - deleteComponentConfiguration(componentPath: string): Promise; - - /** - * Create a component from the given devfile template project. - * - * @param componentPath the folder in which to create the project - * @param componentName the name of the component - * @param portNumber the port number used in the component - * @param devfileName the name of the devfile to use - * @param registryName the name of the devfile registry that the devfile comes from - * @param templateProjectName the template project from the devfile to use - */ - createComponentFromTemplateProject(componentPath: string, componentName: string, portNumber: number, devfileName: string, registryName: string, templateProjectName: string): Promise; - /** - * Create a component from the given local codebase. - * - * @param devfileName the name of the devfile to use - * @param componentName the name of the component - * @param portNumber the port number used in the component - * @param location the location of the local codebase - */ - createComponentFromLocation(devfileName: string, componentName: string, portNumber: number, location: Uri): Promise; -} - -export class OdoImpl implements Odo { - - private static instance: Odo; - - public static get Instance(): Odo { - if (!OdoImpl.instance) { - OdoImpl.instance = new OdoImpl(); - } - return OdoImpl.instance; - } - - async getActiveCluster(): Promise { - const result: CliExitData = await this.execute( - Command.printOdoVersion(), process.cwd(), false - ); - - const odoCluster = result.stdout.trim().split('\n') - .filter((value) => value.includes('Server:')) - .map((value) => { - return value.substring(value.indexOf(':')+1).trim(); - }); - if (odoCluster.length !== 0) { - void commands.executeCommand('setContext', 'isLoggedIn', true); - return odoCluster[0]; - } - - // odo didn't report an active cluster, try reading it from KubeConfig - try { - const kubeConfigCurrentCluster = new KubeConfigUtils().getCurrentCluster().server; - if (kubeConfigCurrentCluster) { - void commands.executeCommand('setContext', 'isLoggedIn', true); - return kubeConfigCurrentCluster; - } - } catch (e) { - // ignored - } - - // no active cluster - void commands.executeCommand('setContext', 'isLoggedIn', false); - } - - async getProjects(): Promise { - return this._listProjects(); - } - - public getKubeconfigEnv(): {KUBECONFIG?: string} { - const addEnv: {KUBECONFIG?: string} = {}; - let kc: KubeConfig; - // TODO: Remove when odo works without kubeconfig present - try { - kc = new KubeConfigUtils(); - } catch (err) { - // ignore error - } - - const configPath = path.join(Platform.getUserHomePath(), '.kube', 'config'); - - if (kc && !pathExistsSync(configPath)) { // config is loaded, yay! But there is still use case for missing config file - // use fake config to let odo get component types from registry - addEnv.KUBECONFIG = path.resolve(__dirname, '..', '..', 'config', 'kubeconfig'); - } - return addEnv; - } - - public async getCompTypesJson(): Promise { - const result: CliExitData = await this.execute(Command.listCatalogComponentsJson(), undefined, true, this.getKubeconfigEnv()); - const componentTypes: DevfileComponentType[] = this.loadJSON(result.stdout); - return componentTypes; - } - - public async getComponentTypes(): Promise { - // if kc is produced, KUBECONFIG env var is empty or pointing - - const result: CliExitData = await this.execute(Command.listCatalogComponentsJson(), undefined, true, this.getKubeconfigEnv()); - const componentTypes: DevfileComponentType[] = this.loadJSON(result.stdout); - const devfileItems: ComponentTypeAdapter[] = []; - - componentTypes.map((item) => devfileItems.push(new ComponentTypeAdapter(item.name, undefined, item.description, undefined, item.registry.name))); - - return devfileItems; - } - - public async describeComponent(contextPath: string, experimental = false): Promise { - const expEnv = experimental ? {ODO_EXPERIMENTAL_MODE: 'true'} : {}; - try { - const describeCmdResult: CliExitData = await this.execute( - Command.describeComponentJson(), contextPath, false, expEnv - ); - return JSON.parse(describeCmdResult.stdout) as ComponentDescription; - } catch(error) { - // ignore and return undefined - } - } - - public async execute(command: CommandText, cwd?: string, fail = true, addEnv = {}): Promise { - const env = cliInstance.CliChannel.createTelemetryEnv(); - const commandActual = `${command}`; - const commandPrivacy = `${command.privacyMode(true)}`; - const [cmd] = commandActual.split(' '); - const toolLocation = await ToolsConfig.detect(cmd); - const result: CliExitData = await ChildProcessUtil.Instance.execute( - toolLocation ? commandActual.replace(cmd, `"${toolLocation}"`) : commandActual, - cwd ? {cwd, env: {...env, ...addEnv}} : { env: {...env, ...addEnv} } - ); - if (result.error && fail) { - throw new VsCommandError(`${result.error.message}`, `Error when running command: ${commandPrivacy}`, result.error); - }; - return result; - } - - public async requireLogin(): Promise { - return await Promise.any([ - this.execute(Command.getCurrentUserName()), - this.execute(Command.listProjects()) - ]).then(() => false).catch(() => true); - } - - public async deleteProject(projectName: string): Promise { - await this.execute(Command.deleteProject(projectName)); - } - - public async createProject(projectName: string): Promise { - await OdoImpl.instance.execute(Command.createProject(projectName)); - // odo handles switching to the newly created namespace/project - } - - public async createComponentFromFolder(type: string, registryName: string, name: string, location: Uri, starter: string = undefined, useExistingDevfile = false, customDevfilePath = ''): Promise { - await this.execute(Command.createLocalComponent(type, registryName, name, undefined, starter, useExistingDevfile, customDevfilePath), location.fsPath); - let wsFolder: WorkspaceFolder; - if (workspace.workspaceFolders) { - // could be new or existing folder - wsFolder = workspace.getWorkspaceFolder(location); - } - if (!workspace.workspaceFolders || !wsFolder) { - workspace.updateWorkspaceFolders(workspace.workspaceFolders? workspace.workspaceFolders.length : 0 , null, { uri: location }); - } - } - - public async createComponentFromLocation(devfileName: string, componentName: string, portNumber: number, location: Uri): Promise { - await this.execute(Command.createLocalComponent(devfileName, undefined, componentName, portNumber, undefined, false, ''), location.fsPath); - } - - public async createComponentFromTemplateProject(componentPath: string, componentName: string, portNumber: number, devfileName: string, registryName: string, templateProjectName: string): Promise { - await this.execute(Command.createLocalComponent(devfileName, registryName, componentName, portNumber, templateProjectName), componentPath); - } - - public async createService(formData: any): Promise { - // create the service under the application 'app', - // which is what odo hardcodes the application to be - // the application is only viewable in the OpenShift Console (website) - formData.metadata.labels = { - app: 'app', - 'app.kubernetes.io/part-of': 'app' - }; - const jsonString = JSON.stringify(formData, null, 4); - const tempJsonFile = tempfile.fileSync({postfix: '.json'}); - fs.writeFileSync(tempJsonFile.name, jsonString); - // call oc create -f path/to/file until odo does support creating services without component - await this.execute(Command.ocCreate(tempJsonFile.name)); - } - - public async analyze(currentFolderPath: string): Promise { - const cliData: CliExitData = await this.execute(Command.analyze(), currentFolderPath); - const parse = JSON.parse(cliData.stdout) as AnalyzeResponse[]; - return parse; - } - - public loadItems(result: CliExitData, fetch: (data) => I[] = (data): I[] => data.items): I[] { - let data: I[] = []; - try { - const items = fetch(JSON.parse(result.stdout)); - if (items) data = items; - } catch (ignore) { - // ignore parse errors and return empty array - } - return data; - } - - private loadJSON(json: string): I { - let data: I; - try { - data = JSON.parse(json,); - } catch (ignore) { - // ignore parse errors and return empty array - } - return data; - } - - public loadItemsFrom(result: CliExitData, fetch: (data:I) => O[] ): O[] { - let data: O[] = []; - try { - const items = fetch(JSON.parse(result.stdout)); - if (items) data = items; - } catch (ignore) { - // ignore parse errors and return empty array - } - return data; - } - - private async loadRegistryFromPreferences() { - const cliData = await this.execute(Command.listRegistries()); - const prefs = JSON.parse(cliData.stdout) as { registries: Registry[]}; - return prefs.registries; - } - - public getRegistries(): Promise { - return this.loadRegistryFromPreferences(); - } - - public async addRegistry(name: string, url: string, token: string): Promise { - await this.execute(Command.addRegistry(name, url, token)); - return { - name, - secure: !!token, - url, - }; - } - - public async removeRegistry(name: string): Promise { - await this.execute(Command.removeRegistry(name)); - } - - public async getActiveProject(): Promise { - const projects = await this._listProjects(); - if (!projects.length) { - return null; - } - const activeProject = projects.find(project => project.active); - return activeProject ? activeProject.name : null; - } - - private async _listProjects(): Promise { - const response = await this.execute(Command.listProjects()); - const responseObj = JSON.parse(response.stdout); - if (!responseObj?.namespaces) { - return []; - } - return responseObj.namespaces as Project[]; - } - - public async deleteComponentConfiguration(componentPath: string): Promise { - await this.execute(Command.deleteComponentConfiguration(), componentPath); - } - - public async canCreatePod(): Promise { - try { - const result: CliExitData = await this.execute(Command.canCreatePod()); - if (result.stdout === 'yes') { - return true; - } - } catch { - //ignore - } - return false; - } - - public async isPodmanPresent(): Promise { - try { - const result: CliExitData = await this.execute(Command.printOdoVersionJson()); - if ('podman' in JSON.parse(result.stdout)) { - return true; - } - } catch { - //ignore - } - return false; - } -} - -export function getInstance(): Odo { - return OdoImpl.Instance; -} diff --git a/src/odo/command.ts b/src/odo/command.ts index edc6ec53b..39b544270 100644 --- a/src/odo/command.ts +++ b/src/odo/command.ts @@ -7,13 +7,6 @@ import { CommandOption, CommandText, verbose } from '../base/command'; export class Command { - static deletePreviouslyPushedResources(name: string): CommandText { - return new CommandText('oc', 'delete deployment', [ - new CommandOption('-l', `component='${name}'`), - new CommandOption('--cascade') - ]) - } - static deploy(): CommandText { return new CommandText('odo', 'deploy'); } @@ -38,134 +31,10 @@ export class Command { return command; } - static ocCreate(fileName: string, namespace?: string): CommandText { - const cmd = new CommandText( - 'oc', - 'create', - [ new CommandOption('-f', fileName), ] - ); - if (namespace) { - cmd.addOption(new CommandOption('--namespace', namespace)); - } - return cmd; - } - - static listProjects(): CommandText { - return new CommandText('odo', 'list project', [ - new CommandOption('-o', 'json', false) - ]); - } - - static getDeployments(namespace: string): CommandText { - return new CommandText( - 'oc', - 'get deployment', - [ - new CommandOption('--namespace', namespace), - new CommandOption('-o', 'json'), - ] - ); - } - - static deleteProject(name: string): CommandText { - return new CommandText( - 'odo', `delete namespace ${name}`, - [ - new CommandOption('-f'), - new CommandOption('-w'), - ] - ); - } - - @verbose - static createProject(name: string): CommandText { - return new CommandText('odo', `create namespace ${name}`, - [ - new CommandOption('-w') - ] - ); - } - - static listComponents(project: string): CommandText { - return new CommandText('odo', - 'list component', [ - new CommandOption('--namespace', project), - new CommandOption('-o', 'json', false) - ] - ); - } - - static listRegistries(): CommandText { - return new CommandText('odo', 'preference view -o json'); - } - - static addRegistry(name: string, url: string, token: string): CommandText { - const cTxt = new CommandText('odo', `preference add registry ${name} ${url}`); - if (token) { - cTxt.addOption(new CommandOption('--token', token)); - } - return cTxt; - } - - static removeRegistry(name: string): CommandText { - return new CommandText('odo', `preference remove registry ${name}`, [new CommandOption('--force')]); - } - - static listCatalogComponentsJson(): CommandText { - return new CommandText('odo', 'registry -o json'); - } - - static listCatalogOperatorBackedServices(): CommandText { - return new CommandText('oc', 'get csv -o jsonpath="{range .items[*]}{.metadata.name}{\'\\t\'}{.spec.version}{\'\\t\'}{.spec.displayName}{\'\\t\'}{.metadata.annotations.description}{\'\\t\'}{.spec.customresourcedefinitions.owned}{\'\\n\'}{end}"'); - } - - static printOcVersion(): CommandText { - return new CommandText('oc', 'version'); - } - - static printOcVersionJson(): CommandText { - return new CommandText('oc', 'version -ojson'); - } - static printOdoVersion(): CommandText { return new CommandText('odo', 'version'); } - static printOdoVersionJson(): CommandText { - return new CommandText('odo', 'version -o json'); - } - - static odoLogout(): CommandText { - return new CommandText('odo', 'logout'); - } - - static setOpenshiftContext(context: string): CommandText { - return new CommandText('oc', `config use-context ${context}`); - } - - static odoLoginWithUsernamePassword( - clusterURL: string, - username: string, - passwd: string, - ): CommandText { - return new CommandText('oc', - `login ${clusterURL}`, [ - new CommandOption('-u', username, true, true), - new CommandOption('-p', passwd, true, true), - new CommandOption('--insecure-skip-tls-verify') - ] - ); - } - - static odoLoginWithToken(clusterURL: string, ocToken: string): CommandText { - return new CommandText('oc', - `login ${clusterURL}`, [ - new CommandOption('--token', ocToken), - new CommandOption('--insecure-skip-tls-verify') - ] - ); - } - static describeComponent(): CommandText { return new CommandText('odo', 'describe component'); } @@ -174,17 +43,6 @@ export class Command { return Command.describeComponent().addOption(new CommandOption('-o', 'json', false)); } - static describeCatalogComponent(component: string, registry: string): CommandText { - return new CommandText('odo', - 'registry', [ - new CommandOption('--details'), - new CommandOption('--devfile', component), - new CommandOption('--devfile-registry', registry), - new CommandOption('-o', 'json', false), - ] - ); - } - static showLog(platform?: string): CommandText { const result = new CommandText('odo', 'logs', [ new CommandOption('--dev'), @@ -238,86 +96,6 @@ export class Command { return cTxt; } - static deleteContext(name: string): CommandText { - return new CommandText('oc', `config delete-context ${name}`); - } - - static deleteCluster(name: string): CommandText { - return new CommandText('oc', `config delete-cluster ${name}`); - } - - static deleteUser(name: string): CommandText { - return new CommandText('oc', `config delete-user ${name}`); - } - - static showServerUrl(): CommandText { - return new CommandText('oc', 'whoami --show-server'); - } - - static getCurrentUserName(): CommandText { - return new CommandText('oc', 'whoami'); - } - - static getCurrentUserToken(): CommandText { - return new CommandText('oc', 'whoami -t'); - } - - /** - * Only works for OpenShift clusters - */ - static showConsoleUrl(): CommandText { - return new CommandText('oc', 'get configmaps console-public -n openshift-config-managed -o json'); - } - - static getClusterServiceVersionJson(name: string) { - return new CommandText('oc', - `get csv ${name}`, [ - new CommandOption('-o', 'json') - ] - ); - } - - static analyze(): CommandText { - return new CommandText('odo', 'analyze -o json'); - } - - static setNamespace(namespace: string) { - return new CommandText('odo', `set namespace ${namespace}`); - } - - static deleteComponentConfiguration(): CommandText { - return new CommandText('odo', 'delete component', [ - new CommandOption('--files'), - new CommandOption('-f'), - ]); - } - - static canCreatePod(): CommandText { - return new CommandText('oc', 'auth can-i create pod'); - } - - static isOpenshiftCluster(): CommandText { - // FIXME: this is POSIX shell and won't work on Windows !! - return new CommandText('oc api-resources | grep openshift'); - } - - static addBinding(serviceNamespace: string, serviceName: string, bindingName: string): CommandText { - return new CommandText('odo', - 'add binding', - [ - new CommandOption('--service-namespace', serviceNamespace, false), - new CommandOption('--service', serviceName, false), - new CommandOption('--name', bindingName, false), - ] - ) - } - - static getBindableServices(): CommandText { - return new CommandText('odo', - 'list service', - [new CommandOption('-o json')]); - } - static runComponentCommand(commandId : string): CommandText { return new CommandText('odo', `run ${commandId}`); } diff --git a/src/odo/componentType.ts b/src/odo/componentType.ts index 6ad020a4c..e65bcd168 100644 --- a/src/odo/componentType.ts +++ b/src/odo/componentType.ts @@ -85,6 +85,7 @@ export interface DevfileData { } export interface AnalyzeResponse { + name: string; devfile: string; devfileRegistry: string; devfileVersion: string; diff --git a/src/odo/odoTypes.ts b/src/odo/odoTypes.ts new file mode 100644 index 000000000..45ac74756 --- /dev/null +++ b/src/odo/odoTypes.ts @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +/** + * Represents an Operator-backed service. + * + * Note that simply installing the Operator doesn't create an instance of the service that the Operator manages. + */ +export interface BindableService { + name: string; + namespace: string; + kind: string; + apiVersion: string; + service: string; +} diff --git a/src/odo/odoWrapper.ts b/src/odo/odoWrapper.ts new file mode 100644 index 000000000..99dce5ed0 --- /dev/null +++ b/src/odo/odoWrapper.ts @@ -0,0 +1,499 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { KubeConfig, KubernetesObject } from '@kubernetes/client-node'; +import { pathExistsSync } from 'fs-extra'; +import * as path from 'path'; +import { Uri, WorkspaceFolder, commands, workspace } from 'vscode'; +import { CommandOption, CommandText } from '../base/command'; +import * as cliInstance from '../cli'; +import { ToolsConfig } from '../tools'; +import { ChildProcessUtil, CliExitData } from '../util/childProcessUtil'; +import { KubeConfigUtils } from '../util/kubeUtils'; +import { Platform } from '../util/platform'; +import { VsCommandError } from '../vscommand'; +import { Command } from './command'; +import { AnalyzeResponse, ComponentTypeAdapter, ComponentTypeDescription, DevfileComponentType, Registry } from './componentType'; +import { ComponentDescription, StarterProject } from './componentTypeDescription'; +import { BindableService } from './odoTypes'; +import { Project } from './project'; + +/** + * Wraps the `odo` cli tool. + */ +export class Odo { + private static instance: Odo; + + public static get Instance(): Odo { + if (!Odo.instance) { + Odo.instance = new Odo(); + } + return Odo.instance; + } + + private constructor() { + // no state + } + + /** + * Returns the URL of the API of the current active cluster, + * or undefined if there are no active clusters. + * + * @return the URL of the API of the current active cluster, + * or undefined if there are no active clusters + */ + public async getActiveCluster(): Promise { + const result: CliExitData = await this.execute( + Command.printOdoVersion(), + process.cwd(), + false, + ); + + const odoCluster = result.stdout + .trim() + .split('\n') + .filter((value) => value.includes('Server:')) + .map((value) => { + return value.substring(value.indexOf(':') + 1).trim(); + }); + if (odoCluster.length !== 0) { + void commands.executeCommand('setContext', 'isLoggedIn', true); + return odoCluster[0]; + } + + // odo didn't report an active cluster, try reading it from KubeConfig + try { + const kubeConfigCurrentCluster = new KubeConfigUtils().getCurrentCluster().server; + if (kubeConfigCurrentCluster) { + void commands.executeCommand('setContext', 'isLoggedIn', true); + return kubeConfigCurrentCluster; + } + } catch (e) { + // ignored + } + + // no active cluster + void commands.executeCommand('setContext', 'isLoggedIn', false); + } + + public async getProjects(): Promise { + return this._listProjects(); + } + + /** + * Changes which project is currently being used. + * + * On non-OpenShift, namespaces are used instead of projects + * + * @param newProject the new project to use + */ + public async setProject(newProject: string): Promise { + await this.execute(new CommandText('odo', `set namespace ${newProject}`), undefined, true); + } + + public getKubeconfigEnv(): { KUBECONFIG?: string } { + const addEnv: { KUBECONFIG?: string } = {}; + let kc: KubeConfig; + // TODO: Remove when odo works without kubeconfig present + try { + kc = new KubeConfigUtils(); + } catch (err) { + // ignore error + } + + const configPath = path.join(Platform.getUserHomePath(), '.kube', 'config'); + + if (kc && !pathExistsSync(configPath)) { + // config is loaded, yay! But there is still use case for missing config file + // use fake config to let odo get component types from registry + addEnv.KUBECONFIG = path.resolve(__dirname, '..', '..', 'config', 'kubeconfig'); + } + return addEnv; + } + + public async getComponentTypes(): Promise { + // if kc is produced, KUBECONFIG env var is empty or pointing + + const result: CliExitData = await this.execute( + new CommandText('odo', 'registry -o json'), + undefined, + true, + this.getKubeconfigEnv(), + ); + const componentTypes: DevfileComponentType[] = this.loadJSON(result.stdout); + const devfileItems: ComponentTypeAdapter[] = []; + + componentTypes.map((item) => + devfileItems.push( + new ComponentTypeAdapter( + item.name, + undefined, + item.description, + undefined, + item.registry.name, + ), + ), + ); + + return devfileItems; + } + + public async describeComponent( + contextPath: string, + experimental = false, + ): Promise { + const expEnv = experimental ? { ODO_EXPERIMENTAL_MODE: 'true' } : {}; + try { + const describeCmdResult: CliExitData = await this.execute( + Command.describeComponentJson(), + contextPath, + false, + expEnv, + ); + return JSON.parse(describeCmdResult.stdout) as ComponentDescription; + } catch (error) { + // ignore and return undefined + } + } + + public async execute( + command: CommandText, + cwd?: string, + fail = true, + addEnv = {}, + ): Promise { + const env = cliInstance.CliChannel.createTelemetryEnv(); + const commandActual = `${command}`; + const commandPrivacy = `${command.privacyMode(true)}`; + const [cmd] = commandActual.split(' '); + const toolLocation = await ToolsConfig.detect(cmd); + const result: CliExitData = await ChildProcessUtil.Instance.execute( + toolLocation ? commandActual.replace(cmd, `"${toolLocation}"`) : commandActual, + cwd ? { cwd, env: { ...env, ...addEnv } } : { env: { ...env, ...addEnv } }, + ); + if (result.error && fail) { + throw new VsCommandError( + `${result.error.message}`, + `Error when running command: ${commandPrivacy}`, + result.error, + ); + } + return result; + } + + public async deleteProject(projectName: string): Promise { + await this.execute( + new CommandText('odo', `delete namespace ${projectName}`, [ + new CommandOption('-f'), + new CommandOption('-w'), + ]), + ); + } + + public async createProject(projectName: string): Promise { + await Odo.instance.execute( + new CommandText('odo', `create namespace ${projectName}`, [new CommandOption('-w')]), + ); + // odo handles switching to the newly created namespace/project + } + + public async createComponentFromFolder( + type: string, + registryName: string, + name: string, + location: Uri, + starter: string = undefined, + useExistingDevfile = false, + customDevfilePath = '', + ): Promise { + await this.execute( + Command.createLocalComponent( + type, + registryName, + name, + undefined, + starter, + useExistingDevfile, + customDevfilePath, + ), + location.fsPath, + ); + let wsFolder: WorkspaceFolder; + if (workspace.workspaceFolders) { + // could be new or existing folder + wsFolder = workspace.getWorkspaceFolder(location); + } + if (!workspace.workspaceFolders || !wsFolder) { + workspace.updateWorkspaceFolders( + workspace.workspaceFolders ? workspace.workspaceFolders.length : 0, + null, + { uri: location }, + ); + } + } + + /** + * Create a component from the given local codebase. + * + * @param devfileName the name of the devfile to use + * @param componentName the name of the component + * @param portNumber the port to expose on the container that runs the code + * @param location the location of the local codebase + */ + public async createComponentFromLocation( + devfileName: string, + componentName: string, + portNumber: number, + location: Uri, + ): Promise { + await this.execute( + Command.createLocalComponent( + devfileName, + undefined, + componentName, + portNumber, + undefined, + false, + '', + ), + location.fsPath, + ); + } + + /** + * Create a component from the given devfile template project. + * + * @param componentPath the folder in which to create the project + * @param componentName the name of the component + * @param portNumber the port to expose on the container that runs the code + * @param devfileName the name of the devfile to use + * @param registryName the name of the devfile registry that the devfile comes from + * @param templateProjectName the template project from the devfile to use + */ + public async createComponentFromTemplateProject( + componentPath: string, + componentName: string, + portNumber: number, + devfileName: string, + registryName: string, + templateProjectName: string, + ): Promise { + await this.execute( + Command.createLocalComponent( + devfileName, + registryName, + componentName, + portNumber, + templateProjectName, + ), + componentPath, + ); + } + + public async analyze(currentFolderPath: string): Promise { + const cliData: CliExitData = await this.execute( + new CommandText('odo', 'analyze -o json'), + currentFolderPath, + ); + const parse = JSON.parse(cliData.stdout) as AnalyzeResponse[]; + return parse; + } + + /** + * Returns a list of starter projects for the given Devfile + * + * TODO: write integration test + * + * @param componentType the Devfile information + * @returns the list of starter projects + */ + public async getStarterProjects( + componentType: ComponentTypeAdapter, + ): Promise { + const descr = await Odo.Instance.execute(this.describeCatalogComponent(componentType)); + try { + const rawJson = JSON.parse(descr.stdout); + const dfCompType = rawJson.find( + (comp) => comp.registry.name === componentType.registryName, + ); + if (dfCompType.devfileData.devfile.starterProjects) { + return dfCompType.devfileData.devfile.starterProjects as StarterProject[]; + } + } catch (ignore) { + // ignore parse errors and return empty array + } + return []; + } + + public async getDetailedComponentInformation( + componentType: ComponentTypeAdapter, + ): Promise { + const result = await this.execute(this.describeCatalogComponent(componentType)); + const [componentTypeDetails] = JSON.parse(result.stdout) as ComponentTypeDescription[]; + return componentTypeDetails; + } + + private loadJSON(json: string): I { + let data: I; + try { + data = JSON.parse(json); + } catch (ignore) { + // ignore parse errors and return empty array + } + return data; + } + + private async loadRegistryFromPreferences() { + const cliData = await this.execute(new CommandText('odo', 'preference view -o json')); + const prefs = JSON.parse(cliData.stdout) as { registries: Registry[] }; + return prefs.registries; + } + + public getRegistries(): Promise { + return this.loadRegistryFromPreferences(); + } + + public async addRegistry(name: string, url: string, token: string): Promise { + const command = new CommandText('odo', `preference add registry ${name} ${url}`); + if (token) { + command.addOption(new CommandOption('--token', token)); + } + await this.execute(command); + return { + name, + secure: !!token, + url, + }; + } + + public async removeRegistry(name: string): Promise { + await this.execute( + new CommandText('odo', `preference remove registry ${name}`, [ + new CommandOption('--force'), + ]), + ); + } + + /** + * Returns the active project or null if no project is active + * + * @returns the active project or null if no project is active + */ + public async getActiveProject(): Promise { + const projects = await this._listProjects(); + if (!projects.length) { + return null; + } + const activeProject = projects.find((project) => project.active); + return activeProject ? activeProject.name : null; + } + + private async _listProjects(): Promise { + const response = await this.execute( + new CommandText('odo', 'list project', [new CommandOption('-o', 'json', false)]), + ); + const responseObj = JSON.parse(response.stdout); + if (!responseObj?.namespaces) { + return []; + } + return responseObj.namespaces as Project[]; + } + + /** + * Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path. + * + * @param componentPath the path to the component + */ + public async deleteComponentConfiguration(componentPath: string): Promise { + await this.execute( + new CommandText('odo', 'delete component', [ + new CommandOption('--files'), + new CommandOption('-f'), + ]), + componentPath, + ); + } + + public async isPodmanPresent(): Promise { + try { + const result: CliExitData = await this.execute( + new CommandText('odo', 'version -o json'), + ); + if ('podman' in JSON.parse(result.stdout)) { + return true; + } + } catch { + //ignore + } + return false; + } + + /** + * Bind a component to a bindable service by modifying the devfile + * + * Resolves when the binding it created. + * + * @param contextPath the path to the component + * @param serviceName the name of the service to bind to + * @param serviceNamespace the namespace the the service is in + * @param bindingName the name of the service binding + */ + public async addBinding( + contextPath: string, + serviceNamespace: string, + serviceName: string, + bindingName: string, + ) { + await this.execute( + new CommandText('odo', 'add binding', [ + new CommandOption('--service-namespace', serviceNamespace, false), + new CommandOption('--service', serviceName, false), + new CommandOption('--name', bindingName, false), + ]), + contextPath, + true, + ); + } + + /** + * Returns a list of all the bindable services on the cluster. + * + * @returns a list of all the bindable services on the cluster + */ + public async getBindableServices(): Promise { + const data: CliExitData = await this.execute( + new CommandText('odo', 'list service', [new CommandOption('-o json')]), + ); + let responseObj; + try { + responseObj = JSON.parse(data.stdout); + } catch { + throw new Error(JSON.parse(data.stderr).message); + } + if (!responseObj.bindableServices) { + return []; + } + return (responseObj.bindableServices as BindableService[]) // + .map((obj) => { + return { + kind: obj.kind, + apiVersion: obj.apiVersion, + metadata: { + namespace: obj.namespace, + name: obj.name, + }, + } as KubernetesObject; + }); + } + + private describeCatalogComponent(componentType: ComponentTypeAdapter): CommandText { + return new CommandText('odo', 'registry', [ + new CommandOption('--details'), + new CommandOption('--devfile', componentType.name), + new CommandOption('--devfile-registry', componentType.registryName), + new CommandOption('-o', 'json', false), + ]); + } +} diff --git a/src/odo/workspace.ts b/src/odo/workspace.ts index 982ee7619..cb5175dce 100644 --- a/src/odo/workspace.ts +++ b/src/odo/workspace.ts @@ -4,8 +4,8 @@ *-----------------------------------------------------------------------------------------------*/ import { EventEmitter, workspace, WorkspaceFolder } from 'vscode'; -import * as odo3 from '../odo3'; import { ComponentDescription } from './componentTypeDescription'; +import { Odo } from './odoWrapper'; export interface ComponentWorkspaceFolder { contextPath: string; @@ -19,7 +19,7 @@ export interface ComponentsChangeEvent { async function mapFoldersToComponents(folders: readonly WorkspaceFolder[]): Promise { const descriptions = folders.map( - (folder) => odo3.newInstance().describeComponent(folder.uri.fsPath) + (folder) => Odo.Instance.describeComponent(folder.uri.fsPath) .then((component: ComponentDescription) => { return { contextPath: folder.uri.fsPath, @@ -97,4 +97,4 @@ export class OdoWorkspace { public findComponent(folder: WorkspaceFolder) { return this.components.find((component) => component.contextPath === folder.uri.fsPath); } -} \ No newline at end of file +} diff --git a/src/odo3.ts b/src/odo3.ts deleted file mode 100644 index aa3dbfb2b..000000000 --- a/src/odo3.ts +++ /dev/null @@ -1,144 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import { KubernetesObject } from '@kubernetes/client-node'; -import { CommandText } from './base/command'; -import { CliChannel } from './cli'; -import { Command as CommonCommand, loadItems } from './k8s/common'; -import { Command as DeploymentCommand } from './k8s/deployment'; -import { DeploymentConfig } from './k8s/deploymentConfig'; -import { Command } from './odo/command'; -import { ComponentDescription } from './odo/componentTypeDescription'; -import { CliExitData } from './util/childProcessUtil'; - -export interface Odo3 { - getNamespaces(): Promise; - getDeployments(): Promise; - getDeploymentConfigs(): Promise; - - setNamespace(newNamespace: string): Promise; - - describeComponent(contextPath: string): Promise; - - /** - * Bind a component to a bindable service by modifying the devfile - * - * Resolves when the binding it created. - * - * @param contextPath the path to the component - * @param serviceName the name of the service to bind to - * @param serviceNamespace the namespace the the service is in - * @param bindingName the name of the service binding - */ - addBinding( - contextPath: string, - serviceName: string, - serviceNamespace: string, - bindingName: string, - ): Promise; - - /** - * Returns a list of all the bindable services on the cluster. - * - * @returns a list of all the bindable services on the cluster - */ - getBindableServices(): Promise; -} - -export class Odo3Impl implements Odo3 { - - private async getListItems(command: CommandText, fail = false) { - const listCliExitData = await CliChannel.getInstance().executeTool(command, undefined, fail); - const result = loadItems(listCliExitData.stdout); - return result; - } - - async getDeployments(): Promise { - return this.getListItems(DeploymentCommand.get()); - } - - async getDeploymentConfigs(): Promise { - return this.getListItems(DeploymentConfig.command.getDeploymentConfigs()); - } - - async getProjectResources() { - return this.getListItems(CommonCommand.getResourceList('project'), true); - } - - async getNamespacesResources() { - return this.getListItems(CommonCommand.getResourceList('namespace'), true); - } - - async getNamespaces(): Promise { - return Promise.any([ - this.getProjectResources(), - this.getNamespacesResources() - ]); - } - - async setNamespace(newNamespace: string): Promise { - await CliChannel.getInstance().executeTool( - Command.setNamespace(newNamespace), undefined, true - ); - } - - public async describeComponent(contextPath: string): Promise { - try { - const describeCmdResult = await CliChannel.getInstance().executeTool( - Command.describeComponentJson(), {cwd: contextPath}, false - ); - return JSON.parse(describeCmdResult.stdout) as ComponentDescription; - } catch(error) { - // ignore and return undefined - } - } - - async addBinding(contextPath: string, serviceNamespace: string, serviceName: string, bindingName: string) { - const myCommand = Command.addBinding(serviceNamespace, serviceName, bindingName); - await CliChannel.getInstance().executeTool( - myCommand, - {cwd: contextPath}, - true - ); - } - - async getBindableServices(): Promise { - const data: CliExitData = await CliChannel.getInstance().executeTool( - Command.getBindableServices() - ); - let responseObj; - try { - responseObj = JSON.parse(data.stdout); - } catch { - throw new Error(JSON.parse(data.stderr).message); - } - if (!responseObj.bindableServices) { - return []; - } - return (responseObj.bindableServices as BindableService[]) // - .map(obj => { - return { - kind: obj.kind, - apiVersion: obj.apiVersion, - metadata: { - namespace: obj.namespace, - name: obj.name, - } - } as KubernetesObject; - }); - } -} - -interface BindableService { - name: string; - namespace: string; - kind: string; - apiVersion: string; - service: string; -} - -export function newInstance(): Odo3 { - return new Odo3Impl(); -} diff --git a/src/openshift/cluster.ts b/src/openshift/cluster.ts index e4f217d52..0d8ba448a 100644 --- a/src/openshift/cluster.ts +++ b/src/openshift/cluster.ts @@ -4,16 +4,17 @@ *-----------------------------------------------------------------------------------------------*/ import { KubernetesObject } from '@kubernetes/client-node'; -import { ExtensionContext, InputBox, QuickInputButton, QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, ThemeIcon, Uri, Progress as VProgress, commands, env, window, workspace } from 'vscode'; +import { ExtensionContext, InputBox, QuickInputButton, QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, ThemeIcon, Uri, commands, env, window, workspace } from 'vscode'; import { CommandText } from '../base/command'; import { CliChannel } from '../cli'; -import { getInstance } from '../odo'; +import { Oc } from '../oc/ocWrapper'; import { Command } from '../odo/command'; +import { Odo } from '../odo/odoWrapper'; import * as NameValidator from '../openshift/nameValidator'; -import { CliExitData } from '../util/childProcessUtil'; import { TokenStore } from '../util/credentialManager'; import { Filters } from '../util/filters'; import { KubeConfigUtils } from '../util/kubeUtils'; +import { LoginUtil } from '../util/loginUtil'; import { Platform } from '../util/platform'; import { Progress } from '../util/progress'; import { VsCommandError, vsCommand } from '../vscommand'; @@ -21,37 +22,45 @@ import { OpenShiftTerminalManager } from '../webview/openshift-terminal/openShif import OpenShiftItem, { clusterRequired } from './openshiftItem'; import fetch = require('make-fetch-happen'); -interface Versions { - 'openshift_version': string; - 'kubernetes_version': string; -} - class quickBtn implements QuickInputButton { constructor(public iconPath: ThemeIcon, public tooltip: string) { } } export class Cluster extends OpenShiftItem { + public static extensionContext: ExtensionContext; - private static cli = CliChannel.getInstance(); + @vsCommand('openshift.explorer.logout') static async logout(): Promise { - const value = await window.showWarningMessage('Do you want to logout of cluster?', 'Logout', 'Cancel'); + const value = await window.showWarningMessage( + 'Do you want to logout of cluster?', + 'Logout', + 'Cancel', + ); if (value === 'Logout') { - return Cluster.cli.executeTool(Command.odoLogout()) - .catch((error) => Promise.reject(new VsCommandError(`Failed to logout of the current cluster with '${error}'!`, 'Failed to logout of the current cluster'))) - .then(async (result) => { - if (result.stderr === '') { + return Oc.Instance.logout() + .catch((error) => + Promise.reject( + new VsCommandError( + `Failed to logout of the current cluster with '${error}'!`, + 'Failed to logout of the current cluster', + ), + ), + ) + .then(async () => { Cluster.explorer.refresh(); Cluster.serverlessView.refresh(); void commands.executeCommand('setContext', 'isLoggedIn', false); - const logoutInfo = await window.showInformationMessage('Successfully logged out. Do you want to login to a new cluster', 'Yes', 'No'); + const logoutInfo = await window.showInformationMessage( + 'Successfully logged out. Do you want to login to a new cluster', + 'Yes', + 'No', + ); if (logoutInfo === 'Yes') { return Cluster.login(undefined, true); } return null; - } - throw new VsCommandError(`Failed to logout of the current cluster with '${result.stderr}'!`, 'Failed to logout of the current cluster'); - }); + }); } return null; } @@ -69,7 +78,7 @@ export class Cluster extends OpenShiftItem { @vsCommand('openshift.oc.about') static async ocAbout(): Promise { - await OpenShiftTerminalManager.getInstance().executeInTerminal(Command.printOcVersion(), undefined, 'Show OKD CLI Tool Version'); + await OpenShiftTerminalManager.getInstance().executeInTerminal(new CommandText('oc', 'version'), undefined, 'Show OKD CLI Tool Version'); } @vsCommand('openshift.output') @@ -77,26 +86,12 @@ export class Cluster extends OpenShiftItem { CliChannel.getInstance().showOutput(); } - static async getConsoleUrl(progress: VProgress<{increment: number, message: string}>): Promise { - let consoleUrl: string; - try { - progress.report({increment: 0, message: 'Detecting cluster type'}); - const getUrlObj = await Cluster.cli.executeTool(Command.showConsoleUrl()); - progress.report({increment: 30, message: 'Getting URL'}); - consoleUrl = JSON.parse(getUrlObj.stdout).data.consoleURL; - } catch (ignore) { - const serverUrl = await Cluster.cli.executeTool(Command.showServerUrl()); - consoleUrl = `${serverUrl.stdout}/console`; - } - return consoleUrl; - } - @vsCommand('openshift.open.developerConsole', true) @clusterRequired() static async openOpenshiftConsole(): Promise { return Progress.execFunctionWithProgress('Opening Console Dashboard', async (progress) => { - const consoleUrl = await Cluster.getConsoleUrl(progress); - progress.report({increment: 100, message: 'Starting default browser'}); + const consoleUrl = (await Oc.Instance.getConsoleInfo()).url; + progress.report({ increment: 100, message: 'Starting default browser' }); return commands.executeCommand('vscode.open', Uri.parse(consoleUrl)); }); } @@ -104,24 +99,39 @@ export class Cluster extends OpenShiftItem { @vsCommand('openshift.open.operatorBackedServiceCatalog') @clusterRequired() static async openOpenshiftConsoleTopography(): Promise { - return Progress.execFunctionWithProgress('Opening Operator Backed Service Catalog', async (progress) => { - const [consoleUrl, namespace] = await Promise.all([ - Cluster.getConsoleUrl(progress), - getInstance().getActiveProject() - ]); - progress.report({increment: 100, message: 'Starting default browser'}); - // eg. https://console-openshift-console.apps-crc.testing/catalog/ns/default?catalogType=OperatorBackedService - return commands.executeCommand('vscode.open', Uri.parse(`${consoleUrl}/catalog/ns/${namespace}?catalogType=OperatorBackedService`)); - }); + return Progress.execFunctionWithProgress( + 'Opening Operator Backed Service Catalog', + async (progress) => { + const [consoleInfo, namespace] = await Promise.all([ + Oc.Instance.getConsoleInfo(), + Odo.Instance.getActiveProject(), + ]); + progress.report({ increment: 100, message: 'Starting default browser' }); + // eg. https://console-openshift-console.apps-crc.testing/catalog/ns/default?catalogType=OperatorBackedService + // FIXME: handle standard k8s dashboard + return commands.executeCommand( + 'vscode.open', + Uri.parse( + `${consoleInfo.url}/catalog/ns/${namespace}?catalogType=OperatorBackedService`, + ), + ); + }, + ); } @vsCommand('openshift.resource.openInDeveloperConsole') @clusterRequired() static async openInDeveloperConsole(resource: KubernetesObject): Promise { return Progress.execFunctionWithProgress('Opening Console Dashboard', async (progress) => { - const consoleUrl = await Cluster.getConsoleUrl(progress); - progress.report({increment: 100, message: 'Starting default browser'}); - return commands.executeCommand('vscode.open', Uri.parse(`${consoleUrl}/topology/ns/${resource.metadata.namespace}?selectId=${resource.metadata.uid}&view=graph`)); + const consoleInfo = await Oc.Instance.getConsoleInfo(); + progress.report({ increment: 100, message: 'Starting default browser' }); + // FIXME: handle standard k8s dashboard + return commands.executeCommand( + 'vscode.open', + Uri.parse( + `${consoleInfo.url}/topology/ns/${resource.metadata.namespace}?selectId=${resource.metadata.uid}&view=graph`, + ), + ); }); } @@ -129,20 +139,30 @@ export class Cluster extends OpenShiftItem { static async switchContext(): Promise { return new Promise((resolve, reject) => { const k8sConfig = new KubeConfigUtils(); - const contexts = k8sConfig.contexts.filter((item) => item.name !== k8sConfig.currentContext); + const contexts = k8sConfig.contexts.filter( + (item) => item.name !== k8sConfig.currentContext, + ); const deleteBtn = new quickBtn(new ThemeIcon('trash'), 'Delete'); const quickPick = window.createQuickPick(); - const contextNames: QuickPickItem[] = contexts.map((ctx) => ({ label: `${ctx.name}`, buttons: [deleteBtn] })); + const contextNames: QuickPickItem[] = contexts.map((ctx) => ({ + label: `${ctx.name}`, + buttons: [deleteBtn], + })); quickPick.items = contextNames; quickPick.buttons = [QuickInputButtons.Back]; if (contextNames.length === 0) { - void window.showInformationMessage('You have no Kubernetes contexts yet, please login to a cluster.', 'Login', 'Cancel') + void window + .showInformationMessage( + 'You have no Kubernetes contexts yet, please login to a cluster.', + 'Login', + 'Cancel', + ) .then((command: string) => { if (command === 'Login') { resolve(Cluster.login(undefined, true)); } resolve(null); - }) + }); } else { let selection: readonly QuickPickItem[] | undefined; const hideDisposable = quickPick.onDidHide(() => resolve(null)); @@ -153,7 +173,7 @@ export class Cluster extends OpenShiftItem { const choice = selection[0]; hideDisposable.dispose(); quickPick.hide(); - Cluster.odo.execute(Command.setOpenshiftContext(choice.label)) + Oc.Instance.setContext(choice.label) .then(() => resolve(`Cluster context is changed to: ${choice.label}.`)) .catch(reject); }); @@ -170,7 +190,7 @@ export class Cluster extends OpenShiftItem { const context = k8sConfig.getContextObject(event.item.label); const index = contexts.indexOf(context); if (index > -1) { - CliChannel.getInstance().executeTool(Command.deleteContext(context.name)) + Oc.Instance.deleteContext(context.name) .then(() => resolve(`Context ${context.name} deleted.`)) .catch(reject); } @@ -195,7 +215,10 @@ export class Cluster extends OpenShiftItem { const createUrl: QuickPickItem = { label: '$(plus) Provide new URL...' }; const clusterItems = k8sConfig.getServers(); const quickPick = window.createQuickPick(); - const contextNames: QuickPickItem[] = clusterItems.map((ctx) => ({ ...ctx, buttons: ctx.description ? [] : [deleteBtn] })); + const contextNames: QuickPickItem[] = clusterItems.map((ctx) => ({ + ...ctx, + buttons: ctx.description ? [] : [deleteBtn], + })); quickPick.items = [createUrl, ...contextNames]; quickPick.buttons = [QuickInputButtons.Back]; let selection: readonly QuickPickItem[] | undefined; @@ -237,10 +260,22 @@ export class Cluster extends OpenShiftItem { // find users and remove duplicates const users = [ ...new Set(contexts.map(context => k8sConfig.getUser(context.user)))]; - await Promise.all(contexts.map((context) => CliChannel.getInstance().executeTool(Command.deleteContext(context.name)))) - .then(() => Promise.all(users.map(user => CliChannel.getInstance().executeTool(Command.deleteUser(user.name))))) - .then(() => CliChannel.getInstance().executeTool(Command.deleteCluster(cluster.name))) - .catch(reject) + await Promise.all( + contexts.map((context) => + Oc.Instance.deleteContext(context.name), + ), + ) + .then(() => + Promise.all( + users.map((user) => + Oc.Instance.deleteUser(user.name) + ), + ), + ) + .then(() => + Oc.Instance.deleteCluster(cluster.name) + ) + .catch(reject); } }); } @@ -254,12 +289,17 @@ export class Cluster extends OpenShiftItem { let pathSelectionDialog; let newPathPrompt; let crcBinary; - const crcPath: string = workspace.getConfiguration('openshiftToolkit').get('crcBinaryLocation'); - if(crcPath) { - newPathPrompt = { label: '$(plus) Provide different OpenShift Local file path'}; - pathSelectionDialog = await window.showQuickPick([{label:`${crcPath}`, description: 'Fetched from settings'}, newPathPrompt], {placeHolder: 'Select OpenShift Local file path', ignoreFocusOut: true}); + const crcPath: string = workspace + .getConfiguration('openshiftToolkit') + .get('crcBinaryLocation'); + if (crcPath) { + newPathPrompt = { label: '$(plus) Provide different OpenShift Local file path' }; + pathSelectionDialog = await window.showQuickPick( + [{ label: `${crcPath}`, description: 'Fetched from settings' }, newPathPrompt], + { placeHolder: 'Select OpenShift Local file path', ignoreFocusOut: true }, + ); } - if(!pathSelectionDialog) return; + if (!pathSelectionDialog) return; if (pathSelectionDialog.label === newPathPrompt.label) { const crcBinaryLocation = await window.showOpenDialog({ canSelectFiles: true, @@ -276,32 +316,6 @@ export class Cluster extends OpenShiftItem { void OpenShiftTerminalManager.getInstance().executeInTerminal(new CommandText(`${crcBinary}`, 'stop'), undefined, 'Stop OpenShift Local'); } - public static async getVersions(): Promise { - const result = await Cluster.cli.executeTool(Command.printOcVersionJson(), undefined, false); - const versions: Versions = { - 'kubernetes_version': undefined, - 'openshift_version': undefined - }; - if (!result.error) { - try { - // try to fetch versions for stdout - const versionsJson = JSON.parse(result.stdout); - if (versionsJson?.serverVersion?.major && versionsJson?.serverVersion?.minor) { - // eslint-disable-next-line camelcase - versions.kubernetes_version = `${versionsJson.serverVersion.major}.${versionsJson.serverVersion.minor}`; - } - if (versionsJson?.openshiftVersion) { - // eslint-disable-next-line camelcase - versions.openshift_version = versionsJson.openshiftVersion; - } - - } catch(err) { - // ignore and return undefined - } - } - return versions; - } - /* * Shows a Quick Pick to select a cluster login method. Returns either: * - login method string, or @@ -433,7 +447,7 @@ export class Cluster extends OpenShiftItem { } case Step.loginUsingCredentials: // Drop down case Step.loginUsingToken: { - let clusterVersions:any = step === Step.loginUsingCredentials + const clusterVersions: string = step === Step.loginUsingCredentials ? await Cluster.credentialsLogin(true, clusterURL) : await Cluster.tokenLogin(clusterURL, true); if (!clusterVersions) { // Back Button is hit @@ -441,13 +455,8 @@ export class Cluster extends OpenShiftItem { } else if (clusterVersions === null) { // User cancelled the operation return null; } else { - const versions = await Cluster.getVersions(); - if (versions) { - clusterVersions = new String(clusterVersions); - // get cluster information using 'oc version' - clusterVersions.properties = versions; - } - return clusterVersions; + // login successful + return null; } break; } @@ -459,21 +468,32 @@ export class Cluster extends OpenShiftItem { private static async requestLoginConfirmation(skipConfirmation = false): Promise { let response = 'Yes'; - if (!skipConfirmation && !await Cluster.odo.requireLogin()) { + if (!skipConfirmation && !(await LoginUtil.Instance.requireLogin())) { const cluster = new KubeConfigUtils().getCurrentCluster(); - response = await window.showInformationMessage(`You are already logged into ${cluster.server} cluster. Do you want to login to a different cluster?`, 'Yes', 'No'); + response = await window.showInformationMessage( + `You are already logged into ${cluster.server} cluster. Do you want to login to a different cluster?`, + 'Yes', + 'No', + ); } return response; } - private static async save(username: string, password: string, checkpassword: string, result: CliExitData): Promise { - if (password === checkpassword) return result; - const response = await window.showInformationMessage('Do you want to save username and password?', 'Yes', 'No'); + private static async save( + username: string, + password: string, + checkpassword: string, + ): Promise { + if (password === checkpassword) return; + const response = await window.showInformationMessage( + 'Do you want to save username and password?', + 'Yes', + 'No', + ); if (response === 'Yes') { await TokenStore.setUserName(username); await TokenStore.setItem('login', username, password); } - return result; } /* @@ -646,16 +666,24 @@ export class Cluster extends OpenShiftItem { // If there is saved password for the username - read it password = await TokenStore.getItem('login', username); try { - const result = await Progress.execFunctionWithProgress( - `Login to the cluster: ${clusterURL}`, - () => Cluster.cli.executeTool(Command.odoLoginWithUsernamePassword(clusterURL, username, passwd), undefined, true)); - await Cluster.save(username, passwd, password, result); - return await Cluster.loginMessage(clusterURL, result); + await Oc.Instance.loginWithUsernamePassword(clusterURL, username, passwd); + await Cluster.save(username, passwd, password); + return await Cluster.loginMessage(clusterURL); } catch (error) { if (error instanceof VsCommandError) { - throw new VsCommandError(`Failed to login to cluster '${clusterURL}' with '${Filters.filterPassword(error.message)}'!`, `Failed to login to cluster. ${error.telemetryMessage}`); + throw new VsCommandError( + `Failed to login to cluster '${clusterURL}' with '${Filters.filterPassword( + error.message, + )}'!`, + `Failed to login to cluster. ${error.telemetryMessage}`, + ); } else { - throw new VsCommandError(`Failed to login to cluster '${clusterURL}' with '${Filters.filterPassword(error.message)}'!`, 'Failed to login to cluster'); + throw new VsCommandError( + `Failed to login to cluster '${clusterURL}' with '${Filters.filterPassword( + error.message, + )}'!`, + 'Failed to login to cluster', + ); } } } @@ -672,12 +700,18 @@ export class Cluster extends OpenShiftItem { static async getUrlFromClipboard(): Promise { const clipboard = await Cluster.readFromClipboard(); - if (NameValidator.ocLoginCommandMatches(clipboard)) return NameValidator.clusterURL(clipboard); + if (NameValidator.ocLoginCommandMatches(clipboard)) { + return NameValidator.clusterURL(clipboard); + } return null; } @vsCommand('openshift.explorer.login.tokenLogin') - static async tokenLogin(userClusterUrl: string, skipConfirmation = false, userToken?: string): Promise { + static async tokenLogin( + userClusterUrl: string, + skipConfirmation = false, + userToken?: string, + ): Promise { let token: string; const response = await Cluster.requestLoginConfirmation(skipConfirmation); @@ -690,7 +724,10 @@ export class Cluster extends OpenShiftItem { clusterUrlFromClipboard = await Cluster.getUrlFromClipboard(); } - if (!clusterURL && clusterUrlFromClipboard || clusterURL?.trim() === clusterUrlFromClipboard) { + if ( + (!clusterURL && clusterUrlFromClipboard) || + clusterURL?.trim() === clusterUrlFromClipboard + ) { token = NameValidator.getToken(await Cluster.readFromClipboard()); clusterURL = clusterUrlFromClipboard; } @@ -713,10 +750,19 @@ export class Cluster extends OpenShiftItem { } else { ocToken = userToken; } - return Progress.execFunctionWithProgress(`Login to the cluster: ${clusterURL}`, - () => Cluster.cli.executeTool(Command.odoLoginWithToken(clusterURL, ocToken.trim())) - .then((result) => Cluster.loginMessage(clusterURL, result)) - .catch((error) => Promise.reject(new VsCommandError(`Failed to login to cluster '${clusterURL}' with '${Filters.filterToken(error.message)}'!`, 'Failed to login to cluster'))) + return Progress.execFunctionWithProgress(`Login to the cluster: ${clusterURL}`, () => + Oc.Instance.loginWithToken(clusterURL, token) + .then(() => Cluster.loginMessage(clusterURL)) + .catch((error) => + Promise.reject( + new VsCommandError( + `Failed to login to cluster '${clusterURL}' with '${Filters.filterToken( + error.message, + )}'!`, + 'Failed to login to cluster', + ), + ), + ), ); } @@ -726,11 +772,16 @@ export class Cluster extends OpenShiftItem { } @vsCommand('openshift.explorer.login.clipboard') - static async loginUsingClipboardToken(apiEndpointUrl: string, oauthRequestTokenUrl: string): Promise { + static async loginUsingClipboardToken( + apiEndpointUrl: string, + oauthRequestTokenUrl: string, + ): Promise { const clipboard = await Cluster.readFromClipboard(); - if(!clipboard) { - const choice = await window.showErrorMessage('Cannot parse token in clipboard. Please click `Get token` button below, copy token into clipboard and press `Login to Sandbox` button again.', - 'Get token'); + if (!clipboard) { + const choice = await window.showErrorMessage( + 'Cannot parse token in clipboard. Please click `Get token` button below, copy token into clipboard and press `Login to Sandbox` button again.', + 'Get token', + ); if (choice === 'Get token') { await commands.executeCommand('vscode.open', Uri.parse(oauthRequestTokenUrl)); } @@ -754,13 +805,10 @@ export class Cluster extends OpenShiftItem { return Cluster.tokenLogin(url, true, token); } - static async loginMessage(clusterURL: string, result: CliExitData): Promise { - if (result.stderr === '') { - Cluster.explorer.refresh(); - Cluster.serverlessView.refresh(); - await commands.executeCommand('setContext', 'isLoggedIn', true); - return `Successfully logged in to '${clusterURL}'`; - } - throw new VsCommandError(result.stderr, 'Failed to login to cluster with output in stderr'); + static async loginMessage(clusterURL: string): Promise { + Cluster.explorer.refresh(); + Cluster.serverlessView.refresh(); + await commands.executeCommand('setContext', 'isLoggedIn', true); + return `Successfully logged in to '${clusterURL}'`; } } diff --git a/src/openshift/component.ts b/src/openshift/component.ts index c0ba97954..5c6f12146 100644 --- a/src/openshift/component.ts +++ b/src/openshift/component.ts @@ -3,18 +3,16 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -/* eslint-disable @typescript-eslint/no-var-requires */ - import * as fs from 'fs/promises'; import * as JSYAML from 'js-yaml'; import * as path from 'path'; import { commands, debug, DebugConfiguration, DebugSession, Disposable, EventEmitter, extensions, ProgressLocation, Uri, window, workspace } from 'vscode'; -import { CliChannel } from '../cli'; +import { Oc } from '../oc/ocWrapper'; import { Command } from '../odo/command'; -import { ascDevfileFirst, ComponentTypeAdapter, ComponentTypeDescription } from '../odo/componentType'; +import { ascDevfileFirst, ComponentTypeAdapter } from '../odo/componentType'; import { CommandProvider, StarterProject } from '../odo/componentTypeDescription'; +import { Odo } from '../odo/odoWrapper'; import { ComponentWorkspaceFolder } from '../odo/workspace'; -import * as odo3 from '../odo3'; import sendTelemetry, { NewComponentCommandProps } from '../telemetry'; import { Progress } from '../util/progress'; import { vsCommand, VsCommandError } from '../vscommand'; @@ -221,10 +219,9 @@ export class Component extends OpenShiftItem { @vsCommand('openshift.component.binding.add') static async addBinding(component: ComponentWorkspaceFolder) { - const odo: odo3.Odo3 = odo3.newInstance(); const services = await Progress.execFunctionWithProgress('Looking for bindable services', (progress) => { - return odo.getBindableServices(); + return Odo.Instance.getBindableServices(); }); if (!services || services.length === 0) { @@ -281,7 +278,7 @@ export class Component extends OpenShiftItem { void sendTelemetry('finishAddBindingWizard'); - await odo.addBinding( + await Odo.Instance.addBinding( component.contextPath, selectedServiceObject.metadata.namespace, selectedServiceObject.metadata.name, @@ -301,7 +298,11 @@ export class Component extends OpenShiftItem { cs.runOn = runOn; Component.stateChanged.fire(component.contextPath) if (!runOn) { - await CliChannel.getInstance().executeTool(Command.deletePreviouslyPushedResources(component.component.devfileData.devfile.metadata.name), undefined, false); + try { + await Oc.Instance.deleteDeploymentByComponentLabel(component.component.devfileData.devfile.metadata.name); + } catch (e) { + // do nothing, it probably was already deleted + } } try { cs.devTerminal = await OpenShiftTerminalManager.getInstance().createTerminal( @@ -534,11 +535,8 @@ export class Component extends OpenShiftItem { } else { progressIndicator.placeholder = 'Loading Starter Projects for selected Component Type' progressIndicator.show(); - const descr = await CliChannel.getInstance().executeTool(Command.describeCatalogComponent(componentType.name, componentType.registryName)); - const starterProjects: StarterProject[] = Component.odo.loadItems(descr, (data: ComponentTypeDescription[]) => { - const dfCompType = data.find((comp) => comp.registry.name === componentType.registryName); - return dfCompType.devfileData.devfile.starterProjects - }); + + const starterProjects: StarterProject[] = await this.odo.getStarterProjects(componentType); progressIndicator.hide(); if (starterProjects?.length && starterProjects.length > 0) { const create = await window.showQuickPick(['Yes', 'No'], { placeHolder: `Initialize Component using ${starterProjects.length === 1 ? '\''.concat(starterProjects[0].name.concat('\' ')) : ''}Starter Project?` }); @@ -563,7 +561,6 @@ export class Component extends OpenShiftItem { const componentName = opts?.compName || await Component.getName( 'Name', - Promise.resolve([]), initialNameValue?.trim().length > 0 ? initialNameValue : createStarter ); diff --git a/src/openshift/openshiftItem.ts b/src/openshift/openshiftItem.ts index 0474960b9..473921321 100644 --- a/src/openshift/openshiftItem.ts +++ b/src/openshift/openshiftItem.ts @@ -5,7 +5,8 @@ import { commands, QuickPickItem, window } from 'vscode'; import { OpenShiftExplorer } from '../explorer'; -import { getInstance, Odo, OpenShiftObject } from '../odo'; +import { Oc } from '../oc/ocWrapper'; +import { Odo } from '../odo/odoWrapper'; import { Project } from '../odo/project'; import { ServerlessFunctionView } from '../serverlessFunction/view'; import * as NameValidator from './nameValidator'; @@ -23,34 +24,21 @@ export class QuickPickCommand implements QuickPickItem { } export default class OpenShiftItem { - protected static readonly odo: Odo = getInstance(); + protected static readonly odo = Odo.Instance; protected static readonly explorer: OpenShiftExplorer = OpenShiftExplorer.getInstance(); protected static readonly serverlessView: ServerlessFunctionView = ServerlessFunctionView.getInstance(); - static validateUniqueName(data: Array, value: string): string { - const openshiftObject = data.find((item) => item.getName() === value); - return openshiftObject && 'This name is already used, please enter different name.'; - } - - static async getName(message: string, data: Promise>, offset?: string, defaultValue = ''): Promise { + static async getName(message: string, offset?: string, defaultValue = ''): Promise { return window.showInputBox({ value: defaultValue, prompt: `Provide ${message}`, ignoreFocusOut: true, - validateInput: async (value: string) => { + validateInput: (value: string) => { let validationMessage = NameValidator.emptyName(`Empty ${message}`, value.trim()); if (!validationMessage) validationMessage = NameValidator.validateMatches(`Not a valid ${message}. Please use lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character`, value); if (!validationMessage) validationMessage = NameValidator.lengthName(`${message} should be between 2-63 characters`, value, offset ? offset.length : 0); - if (!validationMessage) { - try { - const existingResources = await data; - validationMessage = OpenShiftItem.validateUniqueName(existingResources, value); - } catch (err) { - //ignore to keep other validation to work - } - } return validationMessage; } }); @@ -95,7 +83,7 @@ export function clusterRequired() { } descriptor[fnKey] = async function (...args: any[]): Promise { - let hasActiveCluster = await getInstance().canCreatePod(); + let hasActiveCluster = await Oc.Instance.canCreatePod(); if (!hasActiveCluster) { const lOrC = await window.showInformationMessage('Login in to a Cluster to run this command.', 'Login', 'Cancel'); if (lOrC === 'Login') { @@ -103,7 +91,7 @@ export function clusterRequired() { if (typeof loginResult === 'string') { void window.showInformationMessage(loginResult); } - hasActiveCluster = await getInstance().canCreatePod(); + hasActiveCluster = await Oc.Instance.canCreatePod(); } } if (hasActiveCluster) { diff --git a/src/openshift/project.ts b/src/openshift/project.ts index a5bfd2ae0..84facd5e7 100644 --- a/src/openshift/project.ts +++ b/src/openshift/project.ts @@ -5,28 +5,13 @@ import { KubernetesObject } from '@kubernetes/client-node'; import { commands, window } from 'vscode'; -import { CommandOption, CommandText } from '../base/command'; -import { CliChannel } from '../cli'; import { OpenShiftExplorer } from '../explorer'; -import { getInstance as getOdoInstance } from '../odo'; +import { Oc } from '../oc/ocWrapper'; +import { Odo } from '../odo/odoWrapper'; import { Progress } from '../util/progress'; import { VsCommandError, vsCommand } from '../vscommand'; import OpenShiftItem from './openshiftItem'; -export class Command { - static setActiveProject(name: string) { - return new CommandText('odo', `set namespace ${name}`); - } - - static deleteProject(name: string) { - return new CommandText('odo', `delete namespace ${name}`, [new CommandOption('--wait=true'), new CommandOption('-f')]) - } - - static getAll(namespace: string) { - return new CommandText('oc', 'get all', [new CommandOption('--namespace', namespace), new CommandOption('-o', 'json')]); - } -} - export class Project extends OpenShiftItem { @vsCommand('openshift.project.set', true) @@ -36,7 +21,7 @@ export class Project extends OpenShiftItem { label: 'Create new Project', description: 'Create new Project and make it active' }; - const projectsAndCreateNew = getOdoInstance() + const projectsAndCreateNew = Odo.Instance .getProjects() // .then((projects) => [ createNewProject, @@ -51,7 +36,7 @@ export class Project extends OpenShiftItem { await commands.executeCommand('openshift.project.create'); } else { const projectName = selectedItem.label; - await CliChannel.getInstance().executeTool(Command.setActiveProject(projectName)); + await Odo.Instance.setProject(projectName); Project.explorer.refresh(); Project.serverlessView.refresh(); message = `Project '${projectName}' set as active.`; @@ -74,7 +59,7 @@ export class Project extends OpenShiftItem { static async del(project: KubernetesObject): Promise { let result: Promise = null; - const isProjectEmpty = JSON.parse((await getOdoInstance().execute(Command.getAll(project.metadata.name))).stdout).items.length === 0; + const isProjectEmpty = (await Oc.Instance.getAllKubernetesObjects(project.metadata.name)).length === 0; const value = await window.showWarningMessage(`Do you want to delete Project '${project.metadata.name}'${!isProjectEmpty ? ' and all its contents' : ''}?`, 'Yes', 'Cancel'); if (value === 'Yes') { @@ -82,9 +67,9 @@ export class Project extends OpenShiftItem { `Deleting Project '${project.metadata.name}'`, async () => { // migrate to odo3.ts - const projects = await getOdoInstance().getProjects(); + const projects = await Odo.Instance.getProjects(); const selectedProject = projects.find(p => p.name === project.metadata.name); - await getOdoInstance().deleteProject(selectedProject.name); + await Odo.Instance.deleteProject(selectedProject.name); OpenShiftExplorer.getInstance().refresh(); }) .catch((err) => Promise.reject(new VsCommandError(`Failed to delete Project with error '${err}'`,'Failed to delete Project'))) diff --git a/src/registriesView.ts b/src/registriesView.ts index f54b63197..737f59ac3 100644 --- a/src/registriesView.ts +++ b/src/registriesView.ts @@ -10,15 +10,14 @@ import { EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri, window } from 'vscode'; -import { getInstance, Odo, OdoImpl } from './odo'; -import { Command } from './odo/command'; import { + ComponentTypeAdapter, ComponentTypeDescription, DevfileComponentType, Registry } from './odo/componentType'; import { StarterProject } from './odo/componentTypeDescription'; -import { CliExitData } from './util/childProcessUtil'; +import { Odo } from './odo/odoWrapper'; import { Progress } from './util/progress'; import { vsCommand, VsCommandError } from './vscommand'; import fetch = require('make-fetch-happen'); @@ -43,7 +42,7 @@ export class ComponentTypesView implements TreeDataProvider { readonly onDidChangeTreeData: Event = this.onDidChangeTreeDataEmitter.event; - readonly odo: Odo = getInstance(); + readonly odo = Odo.Instance; private registries: Registry[]; private readonly compDescriptions: Set = new Set(); public subject: Subject = new Subject(); @@ -114,18 +113,16 @@ export class ComponentTypesView implements TreeDataProvider { return new Promise((resolve) => { let isError = false; this.compDescriptions.clear(); - void getInstance().getCompTypesJson().then(async (devFileComponentTypes: DevfileComponentType[]) => { + void Odo.Instance.getComponentTypes().then(async (devFileComponentTypes: ComponentTypeAdapter[]) => { await this.getRegistries(); - devFileComponentTypes.forEach((component: DevfileComponentType) => { - getInstance().execute(Command.describeCatalogComponent(component.name, component.registry.name)).then((componentDesc: CliExitData) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const [component] = JSON.parse(componentDesc.stdout) as ComponentTypeDescription[]; - + devFileComponentTypes.forEach((component: ComponentTypeAdapter) => { + Odo.Instance.getDetailedComponentInformation(component) // + .then((componentDesc: ComponentTypeDescription) => { // eslint-disable-next-line max-nested-callbacks - component.devfileData.devfile?.starterProjects?.map((starter: StarterProject) => { + componentDesc.devfileData.devfile?.starterProjects?.map((starter: StarterProject) => { starter.typeName = component.name; }); - this.compDescriptions.add(component); + this.compDescriptions.add(componentDesc); if (devFileComponentTypes.length === this.compDescriptions.size) { this.subject.next('refresh'); @@ -302,7 +299,7 @@ export class ComponentTypesView implements TreeDataProvider { const componentTypes = JSON.parse(await response.text()) as DevfileComponentType[]; if (componentTypes.length > 0) { void Progress.execFunctionWithProgress('Devfile registry is updating',async () => { - const newRegistry = await OdoImpl.Instance.addRegistry(regName, regURL, token); + const newRegistry = await Odo.Instance.addRegistry(regName, regURL, token); ComponentTypesView.instance.addRegistry(newRegistry); await ComponentTypesView.instance.getAllComponents(); ComponentTypesView.instance.refresh(false); @@ -321,7 +318,7 @@ export class ComponentTypesView implements TreeDataProvider { 'No', ); if (yesNo === 'Yes') { - await OdoImpl.Instance.removeRegistry(registry.name); + await Odo.Instance.removeRegistry(registry.name); ComponentTypesView.instance.removeRegistry(registry); if (!isEdit) { await ComponentTypesView.instance.getAllComponents(); diff --git a/src/serverlessFunction/commands.ts b/src/serverlessFunction/commands.ts index 90e6445a4..f0bd5d534 100644 --- a/src/serverlessFunction/commands.ts +++ b/src/serverlessFunction/commands.ts @@ -7,7 +7,7 @@ import * as fs from 'fs-extra'; import { load } from 'js-yaml'; import * as path from 'path'; import { CommandOption, CommandText } from '../base/command'; -import { ClusterVersion, FunctionContent, InvokeFunction } from './types'; +import { FunctionContent, InvokeFunction } from './types'; export class Utils { static async getFuncYamlContent(dir: string): Promise { @@ -67,14 +67,14 @@ export class ServerlessCommand { ]); } - static buildFunction(location: string, image: string, clusterVersion: ClusterVersion | null): CommandText { + static buildFunction(location: string, image: string, isOpenShiftCluster: boolean): CommandText { const commandText = new CommandText('func', 'build', [ new CommandOption('-p', location), new CommandOption('-i', image), new CommandOption('-v') ]); - if (clusterVersion) { - commandText.addOption(new CommandOption('-r', '')) + if (isOpenShiftCluster) { + commandText.addOption(new CommandOption('-r', '""')) } return commandText } @@ -90,7 +90,7 @@ export class ServerlessCommand { static deployFunction(location: string, image: string, namespace: string, - clusterVersion: ClusterVersion | null): CommandText { + isOpenShiftCluster: boolean): CommandText { const commandText = new CommandText('func', 'deploy', [ new CommandOption('-p', location), new CommandOption('-i', image), @@ -99,8 +99,8 @@ export class ServerlessCommand { if (namespace) { commandText.addOption(new CommandOption('-n', namespace)) } - if (clusterVersion) { - commandText.addOption(new CommandOption('-r', '')) + if (isOpenShiftCluster) { + commandText.addOption(new CommandOption('-r', '""')) } return commandText; } @@ -116,12 +116,6 @@ export class ServerlessCommand { return commandText } - static getClusterVersion(): CommandText { - return new CommandText('oc', 'get clusterversion', [ - new CommandOption('-o', 'josn') - ]); - } - static config(functionPath: string, mode: string, isAdd: boolean): CommandText { const option = isAdd ? mode === 'git' ? 'set' : 'add' : 'remove'; const commandText = new CommandText('func', 'config', [ diff --git a/src/serverlessFunction/functions.ts b/src/serverlessFunction/functions.ts index 1b2b5692b..c5355896a 100644 --- a/src/serverlessFunction/functions.ts +++ b/src/serverlessFunction/functions.ts @@ -6,13 +6,14 @@ import validator from 'validator'; import { Uri, commands, window } from 'vscode'; import { CliChannel } from '../cli'; -import { OdoImpl } from '../odo'; +import { Oc } from '../oc/ocWrapper'; +import { Odo } from '../odo/odoWrapper'; import { Platform } from '../util/platform'; import { Progress } from '../util/progress'; import { OpenShiftTerminalApi, OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; import { ServerlessCommand, Utils } from './commands'; import { multiStep } from './multiStepInput'; -import { ClusterVersion, FunctionContent, FunctionObject, FunctionView, InvokeFunction } from './types'; +import { FunctionContent, FunctionObject, FunctionView, InvokeFunction } from './types'; export class Functions { @@ -36,18 +37,6 @@ export class Functions { return this.runTerminalMap.has(`run-${fsPath}`); } - public async checkOpenShiftCluster(): Promise { - try { - const result = await OdoImpl.Instance.execute(ServerlessCommand.getClusterVersion()); - if (result?.stdout?.trim()) { - return JSON.parse(result?.stdout) as ClusterVersion; - } - return null; - } catch (err) { - return null; - } - } - private pollForBuildTerminalDead(resolve: () => void, functionUri: Uri, timeout: number) { return () => { if (!this.buildTerminalMap.get(`build-${functionUri.fsPath}`)) { @@ -83,12 +72,12 @@ export class Functions { } private async buildProcess(context: FunctionObject, view: FunctionView) { - const clusterVersion: ClusterVersion | null = await this.checkOpenShiftCluster(); + const isOpenShiftCluster = await Oc.Instance.isOpenShiftCluster(); const buildImage = await this.getImage(context.folderURI); const terminalKey = `build-${context.folderURI.fsPath}`; const terminal = await OpenShiftTerminalManager.getInstance().createTerminal( - ServerlessCommand.buildFunction(context.folderURI.fsPath, buildImage, clusterVersion), + ServerlessCommand.buildFunction(context.folderURI.fsPath, buildImage, isOpenShiftCluster), `Build ${context.name}`, context.folderURI.fsPath, process.env, @@ -130,7 +119,7 @@ export class Functions { public undeploy(context: FunctionObject) { void Progress.execFunctionWithProgress(`Undeploying the function ${context.name}`, async () => { - const result = await OdoImpl.Instance.execute(ServerlessCommand.undeployFunction(context.name)); + const result = await Odo.Instance.execute(ServerlessCommand.undeployFunction(context.name)); if (result.error) { void window.showErrorMessage(result.error.message); } else { @@ -140,7 +129,7 @@ export class Functions { } public async getTemplates(): Promise { - const result = await OdoImpl.Instance.execute(ServerlessCommand.getTemplates(), undefined, false); + const result = await Odo.Instance.execute(ServerlessCommand.getTemplates(), undefined, false); if (result.error) { void window.showErrorMessage(result.error.message); } @@ -148,7 +137,7 @@ export class Functions { } public async deploy(context: FunctionObject) { - const currentNamespace: string = await OdoImpl.Instance.getActiveProject(); + const currentNamespace: string = await Odo.Instance.getActiveProject(); const yamlContent = await Utils.getFuncYamlContent(context.folderURI.fsPath); if (yamlContent) { const deployedNamespace = yamlContent.deploy?.namespace || undefined; @@ -170,14 +159,14 @@ export class Functions { void window.showErrorMessage(`Function ${context.name} has invalid imaage`) return; } - const clusterVersion: ClusterVersion | null = await this.checkOpenShiftCluster(); + const isOpenShiftCluster = await Oc.Instance.isOpenShiftCluster(); const buildImage = await this.getImage(context.folderURI); // fail after two failed login attempts let triedLoginTwice = false; const terminal = await OpenShiftTerminalManager.getInstance().createTerminal( - ServerlessCommand.deployFunction(context.folderURI.fsPath, buildImage, deployedNamespace, clusterVersion), + ServerlessCommand.deployFunction(context.folderURI.fsPath, buildImage, deployedNamespace, isOpenShiftCluster), `Deploy ${context.name}`, context.folderURI.fsPath, undefined, diff --git a/src/serverlessFunction/manageRepository.ts b/src/serverlessFunction/manageRepository.ts index 6312fc481..5ef6d571d 100644 --- a/src/serverlessFunction/manageRepository.ts +++ b/src/serverlessFunction/manageRepository.ts @@ -4,7 +4,7 @@ *-----------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { OdoImpl } from '../odo'; +import { Odo } from '../odo/odoWrapper'; import sendTelemetry from '../telemetry'; import { ServerlessCommand } from './commands'; @@ -23,7 +23,7 @@ export class ManageRepository { await sendTelemetry('openshift.managerepo.delete', { name }); - const result = await OdoImpl.Instance.execute(ServerlessCommand.deleteRepo(name), '', false); + const result = await Odo.Instance.execute(ServerlessCommand.deleteRepo(name), '', false); if (result.error) { await sendTelemetry('openshift.managerepo.delete.error', { error: result.error.message @@ -39,7 +39,7 @@ export class ManageRepository { oldName, newName }); - const result = await OdoImpl.Instance.execute(ServerlessCommand.renameRepo(oldName, newName), '', false); + const result = await Odo.Instance.execute(ServerlessCommand.renameRepo(oldName, newName), '', false); if (result.error) { await sendTelemetry('openshift.managerepo.rename.error', { error: result.error.message @@ -57,7 +57,7 @@ export class ManageRepository { await sendTelemetry('openshift.managerepo.add', { name, url }); - const result = await OdoImpl.Instance.execute(ServerlessCommand.addRepo(name, url), '', false); + const result = await Odo.Instance.execute(ServerlessCommand.addRepo(name, url), '', false); if (result.error) { await sendTelemetry('openshift.managerepo.add.error', { error: result.error.message @@ -80,7 +80,7 @@ export class ManageRepository { public async list(): Promise { await sendTelemetry('openshift.managerepo.list'); - const result = await OdoImpl.Instance.execute(ServerlessCommand.list(), '', false); + const result = await Odo.Instance.execute(ServerlessCommand.list(), '', false); if (result.error) { await sendTelemetry('openshift.managerepo.list.error', { error: result.error.message diff --git a/src/util/loginUtil.ts b/src/util/loginUtil.ts new file mode 100644 index 000000000..3d6b5ca93 --- /dev/null +++ b/src/util/loginUtil.ts @@ -0,0 +1,31 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { Oc } from '../oc/ocWrapper'; +import { Odo } from '../odo/odoWrapper'; + +export class LoginUtil { + + private static INSTANCE = new LoginUtil(); + + private constructor() { + // no state + } + + public static get Instance(): LoginUtil { + return LoginUtil.INSTANCE; + } + + /** + * Returns true if the user needs to log in to access the cluster, and false otherwise. + * + * @returns true if the user needs to log in to access the cluster, and false otherwise + */ + public async requireLogin(): Promise { + return await Promise.any([Oc.Instance.getCurrentUser(), Odo.Instance.getProjects()]) + .then(() => false) + .catch(() => true); + } +} diff --git a/src/util/swagger.ts b/src/util/swagger.ts index 45235709d..47684aa69 100644 --- a/src/util/swagger.ts +++ b/src/util/swagger.ts @@ -4,8 +4,8 @@ *-----------------------------------------------------------------------------------------------*/ import { JSONSchema7 } from 'json-schema'; -import { Platform } from './platform'; import * as path from 'path'; +import { Platform } from './platform'; const _ = require('lodash'); const https = require('https'); @@ -55,4 +55,4 @@ export function getOpenAPISchemaFor(clusterUrl: string, token: string, kind: str result.definitions = swagger.definitions; // required to resolve oopenapischema references return result; }); -} \ No newline at end of file +} diff --git a/src/webview/add-service-binding/addServiceBindingViewLoader.ts b/src/webview/add-service-binding/addServiceBindingViewLoader.ts index e8002ec46..725a81e54 100644 --- a/src/webview/add-service-binding/addServiceBindingViewLoader.ts +++ b/src/webview/add-service-binding/addServiceBindingViewLoader.ts @@ -4,7 +4,7 @@ *-----------------------------------------------------------------------------------------------*/ import * as path from 'path'; import * as vscode from 'vscode'; -import { getInstance } from '../../odo'; +import { Odo } from '../../odo/odoWrapper'; import { ExtensionID } from '../../util/constants'; import { loadWebviewHtml } from '../common-ext/utils'; @@ -99,7 +99,7 @@ export default class AddServiceBindingViewLoader { }); void panel.webview.postMessage({ action: 'setComponentName', - componentName: (await getInstance().describeComponent(contextPath)).devfileData.devfile.metadata.name, + componentName: (await Odo.Instance.describeComponent(contextPath)).devfileData.devfile.metadata.name, }); return Promise.resolve(panel); diff --git a/src/webview/create-component/createComponentLoader.ts b/src/webview/create-component/createComponentLoader.ts index 72f08edc7..c6507bd26 100644 --- a/src/webview/create-component/createComponentLoader.ts +++ b/src/webview/create-component/createComponentLoader.ts @@ -5,15 +5,15 @@ import * as cp from 'child_process'; import * as fse from 'fs-extra'; import * as fs from 'fs/promises'; +import * as JSYAML from 'js-yaml'; import * as path from 'path'; import * as tmp from 'tmp'; import { promisify } from 'util'; import * as vscode from 'vscode'; import { extensions, Uri, ViewColumn, WebviewPanel, window } from 'vscode'; -import * as JSYAML from 'js-yaml'; -import { OdoImpl } from '../../odo'; import { AnalyzeResponse, ComponentTypeDescription } from '../../odo/componentType'; import { Endpoint } from '../../odo/componentTypeDescription'; +import { Odo } from '../../odo/odoWrapper'; import { ComponentTypesView } from '../../registriesView'; import sendTelemetry from '../../telemetry'; import { ExtensionID } from '../../util/constants'; @@ -21,8 +21,8 @@ import { DevfileConverter } from '../../util/devfileConverter'; import { selectWorkspaceFolder } from '../../util/workspace'; import { getDevfileCapabilities, - getDevfileTags, getDevfileRegistries, + getDevfileTags, isValidProjectFolder, validateComponentName, validatePortNumber @@ -304,7 +304,7 @@ export default class CreateComponentLoader { message.data.templateProject; componentFolder = path.join(projectFolder, componentName); await fs.mkdir(componentFolder); - await OdoImpl.Instance.createComponentFromTemplateProject( + await Odo.Instance.createComponentFromTemplateProject( componentFolder, componentName, portNumber, @@ -338,7 +338,7 @@ export default class CreateComponentLoader { } const devfileType = getDevfileType(message.data.devfileDisplayName); if (!await isDevfileExists(Uri.file(componentFolder))) { - await OdoImpl.Instance.createComponentFromLocation( + await Odo.Instance.createComponentFromLocation( devfileType, componentName, portNumber, @@ -431,7 +431,7 @@ export default class CreateComponentLoader { void CreateComponentLoader.panel.webview.postMessage({ action: 'getRecommendedDevfileStart' }); - analyzeRes = await OdoImpl.Instance.analyze(uri.fsPath); + analyzeRes = await Odo.Instance.analyze(uri.fsPath); compDescriptions = getCompDescription(analyzeRes); } catch (error) { if (error.message.toLowerCase().indexOf('failed to parse the devfile') !== -1) { @@ -446,7 +446,7 @@ export default class CreateComponentLoader { const file = await fs.readFile(devFileV1Path, 'utf8'); const devfileV1 = JSYAML.load(file.toString()); await fs.unlink(devFileV1Path); - analyzeRes = await OdoImpl.Instance.analyze(uri.fsPath); + analyzeRes = await Odo.Instance.analyze(uri.fsPath); compDescriptions = getCompDescription(analyzeRes); const endPoints = getEndPoints(compDescriptions[0]); const devfileV2 = DevfileConverter.getInstance().devfileV1toDevfileV2( diff --git a/src/webview/devfile-registry/registryViewLoader.ts b/src/webview/devfile-registry/registryViewLoader.ts index f4d4d34ec..58ace81e6 100644 --- a/src/webview/devfile-registry/registryViewLoader.ts +++ b/src/webview/devfile-registry/registryViewLoader.ts @@ -5,8 +5,8 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as vscode from 'vscode'; -import { OdoImpl } from '../../odo'; import { Registry } from '../../odo/componentType'; +import { Odo } from '../../odo/odoWrapper'; import { ComponentTypesView } from '../../registriesView'; import sendTelemetry from '../../telemetry'; import { ExtensionID } from '../../util/constants'; @@ -51,7 +51,7 @@ async function devfileRegistryViewerMessageListener(event: any): Promise { const componentFolder = path.join(projectFolder, componentName); try { await fs.mkdir(componentFolder); - await OdoImpl.Instance.createComponentFromTemplateProject( + await Odo.Instance.createComponentFromTemplateProject( componentFolder, componentName, portNumber, diff --git a/src/webview/serverless-function/serverlessFunctionLoader.ts b/src/webview/serverless-function/serverlessFunctionLoader.ts index 7ea5e0508..2b8306560 100644 --- a/src/webview/serverless-function/serverlessFunctionLoader.ts +++ b/src/webview/serverless-function/serverlessFunctionLoader.ts @@ -8,7 +8,7 @@ import * as fs from 'fs/promises'; import * as JSYAML from 'js-yaml'; import * as path from 'path'; import * as vscode from 'vscode'; -import { OdoImpl } from '../../odo'; +import { Odo } from '../../odo/odoWrapper'; import { ServerlessCommand, Utils } from '../../serverlessFunction/commands'; import { Functions } from '../../serverlessFunction/functions'; import { InvokeFunction } from '../../serverlessFunction/types'; @@ -236,7 +236,7 @@ export default class ServerlessFunctionViewLoader { ): Promise { let functionResponse: CliExitData; try { - const response = await OdoImpl.Instance.execute( + const response = await Odo.Instance.execute( ServerlessCommand.createFunction(language, template, location), ); if (response && !response.error) { diff --git a/src/oc.ts b/src/yamlFileCommands.ts similarity index 75% rename from src/oc.ts rename to src/yamlFileCommands.ts index c417117db..bcebfcf04 100644 --- a/src/oc.ts +++ b/src/yamlFileCommands.ts @@ -4,15 +4,14 @@ *-----------------------------------------------------------------------------------------------*/ import { window } from 'vscode'; -import { getInstance } from './odo'; -import { Command } from './odo/command'; +import { Oc as OcWrapper } from './oc/ocWrapper'; +import { Odo } from './odo/odoWrapper'; import { clusterRequired } from './openshift/openshiftItem'; -import { ToolsConfig } from './tools'; import { vsCommand } from './vscommand'; -export class Oc { +export class YamlFileCommands { - private static odo = getInstance(); + private static odo = Odo.Instance; @vsCommand('openshift.create') @clusterRequired() @@ -41,26 +40,18 @@ export class Oc { } } - const activeProject = await Oc.odo.getActiveProject(); + const activeProject = await YamlFileCommands.odo.getActiveProject(); if (!message && !activeProject) { message = '\'OpenShift: Create\' requires setting a project as active, and none is currently set.'; } - let toolLocation: string; - if (!message) { - toolLocation = await ToolsConfig.detect('oc'); - if (!toolLocation) { - message = 'Cannot run \'oc create\'. OKD CLI client tool cannot be found.'; - } - } - if (message) { void window.showWarningMessage(message); return null; } - await Oc.odo.execute(Command.ocCreate(document.fileName, activeProject)); + await OcWrapper.Instance.createKubernetesObjectFromFile(document.fileName); return 'Resources were successfully created.'; } } diff --git a/test/integration/command.test.ts b/test/integration/command.test.ts index aac4c8f36..42220c64b 100644 --- a/test/integration/command.test.ts +++ b/test/integration/command.test.ts @@ -4,7 +4,6 @@ *-----------------------------------------------------------------------------------------------*/ import { V220Devfile, V220DevfileCommandsItemsExecGroup } from '@devfile/api'; -import { KubeConfig } from '@kubernetes/client-node'; import { fail } from 'assert'; import { assert, expect } from 'chai'; import { ChildProcess } from 'child_process'; @@ -16,19 +15,19 @@ import { promisify } from 'util'; import { EventEmitter, Terminal, window, workspace } from 'vscode'; import { CommandText } from '../../src/base/command'; import { CliChannel } from '../../src/cli'; -import { getInstance } from '../../src/odo'; +import { Oc } from '../../src/oc/ocWrapper'; import { Command } from '../../src/odo/command'; import { ComponentDescription } from '../../src/odo/componentTypeDescription'; +import { Odo } from '../../src/odo/odoWrapper'; -const ODO = getInstance(); -const kc = new KubeConfig(); +const ODO = Odo.Instance; const newProjectName = `project${Math.round(Math.random() * 1000)}`; // tests are assuming your current context is already pointing to test cluster on which you can create and delete namespaces suite('odo commands integration', function () { - const isOpenShift = process.env.IS_OPENSHIFT || false; + const isOpenShift: boolean = Boolean(process.env.IS_OPENSHIFT) || false; const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; const username = process.env.CLUSTER_USER || 'developer'; const password = process.env.CLUSTER_PASSWORD || 'developer'; @@ -36,267 +35,22 @@ suite('odo commands integration', function () { suiteSetup(async function() { if (isOpenShift) { try { - await ODO.execute(Command.odoLogout()); + await Oc.Instance.logout(); } catch (e) { // do nothing } - await ODO.execute( - Command.odoLoginWithUsernamePassword( - clusterUrl, - username, - password, - ), - ); - } - kc.loadFromDefault(); - }); - - suite('login/logout', function() { - let token: string; - - suiteSetup(async function() { - if (isOpenShift) { - // get current user token and logout - await ODO.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - token = (await ODO.execute(Command.getCurrentUserToken())).stdout; - await ODO.execute(Command.odoLogout()); - } else { - this.skip(); - } - }); - - suiteTeardown(async function() { - // log back in for the rest of the tests - if (isOpenShift) { - await ODO.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - }); - - teardown(async function() { - await ODO.execute(Command.odoLogout()); - }); - - test('odoLogout()', async function() { - try { - await ODO.execute(Command.getCurrentUserName()) - expect.fail('should be unable to get current user, since you are logged out'); - } catch (_e) { - // do nothing - } - }); - - test('odoLoginWithUsernamePassword()', async function () { - await ODO.execute( - Command.odoLoginWithUsernamePassword( - clusterUrl, - username, - password, - ), + await Oc.Instance.loginWithUsernamePassword( + clusterUrl, + username, + password, ); - const currentUserData = await ODO.execute(Command.getCurrentUserName()); - expect(currentUserData.stdout).to.equal(username); - }); - - test('odoLoginWithToken()', async function() { - await ODO.execute(Command.odoLoginWithToken(clusterUrl, token)); - const currentUserData = await ODO.execute(Command.getCurrentUserName()); - expect(currentUserData.stdout).to.equal(username); - }); - - }); - - test('showServerUrl()', async function() { - const cliData = await ODO.execute(Command.showServerUrl()); - expect(cliData.stdout).to.equal(clusterUrl); - }); - - test('getCurrentUserName()', async function() { - if (isOpenShift) { - const cliData = await ODO.execute(Command.getCurrentUserName()); - expect(cliData.stdout).to.contain(username); - } else { - this.skip(); - } - }); - - test('getCurrentUserToken()', async function() { - if (isOpenShift) { - await ODO.execute(Command.getCurrentUserToken()); - } else { - this.skip(); } }); - suite('project-related commands', function() { - - suiteSetup(async function() { - // createProject() - await ODO.execute(Command.createProject(newProjectName)); - }); - - suiteTeardown(async function() { - // deleteProject() - await ODO.execute(Command.deleteProject(newProjectName)); - }); - - test('listProjects()', function () { - return ODO.execute(Command.listProjects()); - }); - - test('getDeployments()', async function () { - await ODO.execute(Command.getDeployments(newProjectName)); - }); - - test('setNamespace()', async function() { - await ODO.execute(Command.setNamespace(newProjectName)); - }); - - }); - - test('listRegistries()', async function () { - return ODO.execute(Command.listRegistries()); - }); - - test('addRegistry()', async function() { - await ODO.execute(Command.addRegistry('CheRegistry', 'https://example.org', undefined)); - }); - - test('removeRegistry()', async function() { - await ODO.execute(Command.removeRegistry('CheRegistry')); - }); - - test('listCatalogComponentsJson()', async function () { - await ODO.execute(Command.listCatalogComponentsJson()); - }); - - test('printOcVersion()', async function () { - await ODO.execute(Command.printOcVersion()); - }); - - test('printOcVersionJson()', async function() { - await ODO.execute(Command.printOcVersionJson()); - }); - test('printOdoVersion()', async function () { await ODO.execute(Command.printOdoVersion()); }); - test('showServerUrl()', async function () { - await ODO.execute(Command.showServerUrl()); - }); - - test('showConsoleUrl()', async function () { - if (isOpenShift) { - const canI = await ODO.execute( - new CommandText('oc', - 'auth can-i get configmap --namespace openshift-config-managed'), - undefined, - false, - ).then((result) => { - return !result.stdout.startsWith('no'); - }); - if (!canI) { - this.skip(); - } else { - await ODO.execute(Command.showConsoleUrl()); - } - } else { - this.skip(); - } - }); - - test('describeCatalogComponent()', async function () { - const types = await ODO.getComponentTypes(); - const devfileCompType = types[0]; - if (!devfileCompType) { - this.skip(); - } else { - await ODO.execute( - Command.describeCatalogComponent( - devfileCompType.name, - devfileCompType.registryName, - ), - ); - } - }); - - test('setOpenShiftContext', async function () { - await ODO.execute(Command.setOpenshiftContext(kc.currentContext)); - }); - - test('getBindableServices()', async function() { - const result = await ODO.execute(Command.getBindableServices()); - expect(result.stdout.trim()).to.equal('{}'); - }); - - test.skip('deletePreviouslyPushedResources()'); - test.skip('listCatalogOperatorBackedServices()'); - test.skip('addHelmRepo()'); - test.skip('updateHelmRepo()'); - test.skip('installHelmChart()'); - test.skip('unInstallHelmChart()'); - test.skip('deleteComponentNoContext()'); - test.skip('deleteContext()'); - test.skip('deleteCluster()'); - test.skip('deleteUser()'); - test.skip('getClusterServiceVersionJson()'); - - suite('services', function() { - - const serviceName = 'my-test-service'; - const projectName = 'my-test-service-project1'; - - // taken from https://docs.openshift.com/container-platform/3.11/dev_guide/deployments/kubernetes_deployments.html - const serviceFileYaml = // - 'apiVersion: apps/v1\n' + // - 'kind: Deployment\n' + // - 'metadata:\n' + // - ` name: ${serviceName}\n` + // - 'spec:\n' + // - ' replicas: 1\n' + // - ' selector:\n' + // - ' matchLabels:\n' + // - ' app: hello-openshift\n' + // - ' template:\n' + // - ' metadata:\n' + // - ' labels:\n' + // - ' app: hello-openshift\n' + // - ' spec:\n' + // - ' containers:\n' + // - ' - name: hello-openshift\n' + // - ' image: openshift/hello-openshift:latest\n' + // - ' ports:\n' + // - ' - containerPort: 80\n'; - - let serviceFile: string; - - suiteSetup(async function () { - serviceFile = await promisify(tmp.file)(); - await fs.writeFile(serviceFile, serviceFileYaml); - if (isOpenShift) { - await ODO.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - try { - await ODO.execute(Command.createProject(projectName)); - } catch (e) { - // do nothing, it probably already exists - } - await ODO.execute(Command.setNamespace(projectName)); - }); - - suiteTeardown(async function() { - await ODO.execute(new CommandText(`oc delete deployment ${serviceName} --namespace ${projectName} --force=true`)); - await fs.rm(serviceFile); - // this call fails to exit on kind/minikube during integration tests - void ODO.deleteProject(projectName); - }); - - test('ocCreate()', async function() { - await ODO.execute(Command.ocCreate(serviceFile)); - }); - - }); - suite('component', function() { const componentName = 'my-test-component'; const componentType = 'go'; @@ -304,11 +58,11 @@ suite('odo commands integration', function () { let componentLocation: string; suiteSetup(async function () { - await ODO.execute(Command.createProject(newProjectName)); - await ODO.execute(Command.setNamespace(newProjectName)); + await Odo.Instance.createProject(newProjectName); + await Odo.Instance.setProject(newProjectName); componentLocation = await promisify(tmp.dir)(); if (isOpenShift) { - await ODO.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); } }); @@ -324,7 +78,7 @@ suite('odo commands integration', function () { workspace.updateWorkspaceFolders(toRemove, 1); } await fs.rm(componentLocation, { recursive: true, force: true }); - await ODO.execute(Command.deleteProject(newProjectName)); + await Odo.Instance.deleteProject(newProjectName); }); test('createLocalComponent()', async function () { @@ -344,10 +98,6 @@ suite('odo commands integration', function () { await fs.access(path.join(componentLocation, 'devfile.yaml')); }); - test('listComponents()', async function () { - await ODO.execute(Command.listComponents(newProjectName)); - }); - test('describeComponent()', async function() { const res = await ODO.execute(Command.describeComponent(), componentLocation); expect(res.stdout).contains(componentName); @@ -360,13 +110,6 @@ suite('odo commands integration', function () { expect(res.stdout).contains(componentType); }); - test('analyze()', async function() { - const res = await ODO.execute(Command.analyze(), componentLocation); - const resObj = JSON.parse(res.stdout); - expect(resObj[0]?.name).to.equal(path.basename(componentLocation).toLocaleLowerCase()); - expect(resObj[0]?.devfile).to.equal(componentType); - }); - suite('deploying', function() { // FIXME: Deploy depends on pushing container images to a registry. // The default registry it tries to push to is docker. @@ -461,21 +204,6 @@ suite('odo commands integration', function () { term.sendText('a'); term.dispose(); }); - - test('addBinding()', async function() { - const result = await ODO.execute(Command.addBinding('default', 'myservice', 'myservice-binding'), componentLocation, false); - expect(result.stderr).to.contain('No bindable service instances found in namespace "default"'); - }); - - test('deleteComponentConfiguration', async function() { - await ODO.execute(Command.deleteComponentConfiguration(), componentLocation); - try { - await fs.access(path.join(componentLocation, 'devfile.yaml')); - this.fail(); - } catch (_ignored) { - // do nothing - } - }); }); suite('component dev', function() { @@ -486,12 +214,14 @@ suite('odo commands integration', function () { suiteSetup(async function () { if (isOpenShift) { - await ODO.execute( - Command.odoLoginWithUsernamePassword(clusterUrl, username, password), - ); + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + try { + await Odo.Instance.createProject(newProjectName); + } catch (e) { + // do nothing; already exists } - await ODO.execute(Command.createProject(newProjectName)); - await ODO.execute(Command.setNamespace(newProjectName)); + await Odo.Instance.setProject(newProjectName); componentLocation = await promisify(tmp.dir)(); }); @@ -507,7 +237,7 @@ suite('odo commands integration', function () { workspace.updateWorkspaceFolders(toRemove, 1); } await fs.rm(componentLocation, { recursive: true, force: true }); - await ODO.execute(Command.deleteProject(newProjectName)); + await Odo.Instance.deleteProject(newProjectName); }); interface TerminalListener { diff --git a/test/integration/helm.test.ts b/test/integration/helm.test.ts index ddb256294..e178036d4 100644 --- a/test/integration/helm.test.ts +++ b/test/integration/helm.test.ts @@ -7,16 +7,16 @@ import { expect } from 'chai'; import { CommandText } from '../../src/base/command'; import { CliChannel } from '../../src/cli'; import * as Helm from '../../src/helm/helm'; -import { getInstance } from '../../src/odo'; -import { Command } from '../../src/odo/command'; +import { Oc } from '../../src/oc/ocWrapper'; +import { Odo } from '../../src/odo/odoWrapper'; suite('helm integration', function () { - const isOpenShift = process.env.IS_OPENSHIFT || false; + const isOpenShift: boolean = Boolean(process.env.IS_OPENSHIFT) || false; const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; const username = process.env.CLUSTER_USER || 'developer'; const password = process.env.CLUSTER_PASSWORD || 'developer'; - const odo = getInstance(); + const odo = Odo.Instance; const RELEASE_NAME = 'my-helm-release'; const CHART_NAME = 'fredco-samplechart'; @@ -25,7 +25,7 @@ suite('helm integration', function () { suiteSetup(async function () { if (isOpenShift) { - await odo.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); } try { await odo.deleteProject(HELM_NAMESPACE); @@ -44,7 +44,7 @@ suite('helm integration', function () { // this call fails to exit on minikube/kind void odo.deleteProject(HELM_NAMESPACE); if (isOpenShift) { - await odo.execute(Command.odoLogout()); + await Oc.Instance.logout(); } }); diff --git a/test/integration/loginUtils.test.ts b/test/integration/loginUtils.test.ts new file mode 100644 index 000000000..4a51fab33 --- /dev/null +++ b/test/integration/loginUtils.test.ts @@ -0,0 +1,36 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { expect } from 'chai'; +import { Oc } from '../../src/oc/ocWrapper'; +import { LoginUtil } from '../../src/util/loginUtil'; + +suite('loginUtil.ts', function () { + + const isOpenShift: boolean = Boolean(process.env.IS_OPENSHIFT) || false; + const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; + const username = process.env.CLUSTER_USER || 'developer'; + const password = process.env.CLUSTER_PASSWORD || 'developer'; + + suiteSetup(async function () { + if (!isOpenShift) { + this.skip(); + } else { + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + }); + + suiteTeardown(async function () { + if (isOpenShift) { + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + }); + + test('requireLogin()', async function () { + expect(await LoginUtil.Instance.requireLogin()).to.be.false; + await Oc.Instance.logout(); + expect(await LoginUtil.Instance.requireLogin()).to.be.true; + }); +}); diff --git a/test/integration/ocWrapper.test.ts b/test/integration/ocWrapper.test.ts new file mode 100644 index 000000000..305111d83 --- /dev/null +++ b/test/integration/ocWrapper.test.ts @@ -0,0 +1,246 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { KubeConfig } from '@kubernetes/client-node'; +import { expect } from 'chai'; +import * as fs from 'fs/promises'; +import * as JSYAML from 'js-yaml'; +import * as tmp from 'tmp'; +import { promisify } from 'util'; +import { Oc } from '../../src/oc/ocWrapper'; +import { ClusterType } from '../../src/oc/types'; +import { Odo } from '../../src/odo/odoWrapper'; + +suite('./oc/ocWrapper.ts', function () { + const isOpenShift: boolean = Boolean(process.env.IS_OPENSHIFT) || false; + const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; + const username = process.env.CLUSTER_USER || 'developer'; + const password = process.env.CLUSTER_PASSWORD || 'developer'; + + const PROJECT = 'my-test-project-1'; + + suiteSetup(async function () { + if (isOpenShift) { + try { + await Oc.Instance.logout(); + } catch (e) { + // do nothing + } + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + try { + await Odo.Instance.createProject(PROJECT); + } catch (e) { + // do nothing, it probably already exists + } + }); + + suiteTeardown(async function () { + // ensure projects are cleaned up + try { + await Odo.Instance.deleteProject(PROJECT); + } catch (e) { + // do nothing + } + + if (isOpenShift) { + await Oc.Instance.logout(); + } + }); + + test('canCreatePod()', async function () { + const canCreatePod1 = await Oc.Instance.canCreatePod(); + expect(canCreatePod1).to.exist; + expect(canCreatePod1).to.equal(true); + if (isOpenShift) { + await Oc.Instance.logout(); + const canCreatePod2 = await Oc.Instance.canCreatePod(); + expect(canCreatePod2).to.exist; + expect(canCreatePod2).to.equal(false); + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + }); + + suite('create, list, and delete kubernetes objects', function () { + const serviceName = 'my-test-service'; + const projectName = 'my-test-service-project2'; + const componentName = 'my-component'; + + let yamlFile: string; + + // taken from https://docs.openshift.com/container-platform/3.11/dev_guide/deployments/kubernetes_deployments.html + const serviceFileYaml = // + 'apiVersion: apps/v1\n' + // + 'kind: Deployment\n' + // + 'metadata:\n' + // + ` name: ${serviceName}\n` + // + ' labels:\n' + // + ` component: ${componentName}\n` + // + 'spec:\n' + // + ' replicas: 1\n' + // + ' selector:\n' + // + ' matchLabels:\n' + // + ' app: hello-openshift\n' + // + ' template:\n' + // + ' metadata:\n' + // + ' labels:\n' + // + ' app: hello-openshift\n' + // + ' spec:\n' + // + ' containers:\n' + // + ' - name: hello-openshift\n' + // + ' image: openshift/hello-openshift:latest\n' + // + ' ports:\n' + // + ' - containerPort: 80\n'; + + suiteSetup(async function () { + yamlFile = await promisify(tmp.file)(); + await fs.writeFile(yamlFile, serviceFileYaml); + try { + await Odo.Instance.deleteProject(projectName); + } catch (e) { + // do nothing + } + try { + await Odo.Instance.createProject(projectName); + } catch (e) { + // do nothing + } + await Odo.Instance.setProject(projectName); + }); + + suiteTeardown(async function () { + await fs.unlink(yamlFile); + // this call fails to exit on minikube/kind + void Odo.Instance.deleteProject(projectName); + }); + + test('createKubernetesObjectFromSpec()', async function () { + let deployments = await Oc.Instance.getKubernetesObjects('Deployment'); + expect(deployments).to.have.length(0); + await Oc.Instance.createKubernetesObjectFromSpec(JSYAML.load(serviceFileYaml) as object); + deployments = await Oc.Instance.getKubernetesObjects('Deployment'); + expect(deployments).to.have.length(1); + }); + + test('getAllKubernetesObjects()', async function () { + const objects = await Oc.Instance.getAllKubernetesObjects(); + // there should also be a service and potentially a pod + expect(objects).to.have.length.greaterThan(1); + }); + + test('getKubernetesObject()', async function () { + const kubernetesObject = await Oc.Instance.getKubernetesObject('Deployment', serviceName, projectName); + expect((kubernetesObject as any).metadata.labels.component).to.equal(componentName); + }); + + test('deleteKubernetesObject()', async function() { + await Oc.Instance.deleteKubernetesObject('Deployment', serviceName); + const deployments = await Oc.Instance.getKubernetesObjects('Deployment'); + expect(deployments).to.have.length(0); + }); + + test('createKubernetesObjectFromFile()', async function() { + await Oc.Instance.createKubernetesObjectFromFile(yamlFile); + const deployments = await Oc.Instance.getKubernetesObjects('Deployment'); + expect(deployments).to.have.length(1); + }); + + test('deleteDeploymentByComponentLabel', async function() { + await Oc.Instance.deleteDeploymentByComponentLabel(componentName); + const deployments = await Oc.Instance.getKubernetesObjects('Deployment'); + expect(deployments).to.have.length(0); + }); + }); + + test('getConsoleInfo()', async function() { + const consoleInfo = await Oc.Instance.getConsoleInfo(); + if (isOpenShift) { + expect(consoleInfo.kind).to.equal(ClusterType.OpenShift); + } else { + expect(consoleInfo.kind).to.equal(ClusterType.Kubernetes); + } + expect(consoleInfo.url.startsWith(clusterUrl)); + }); + + test('isOpenShiftCluster()', async function() { + const checkIsOpenShift = await Oc.Instance.isOpenShiftCluster(); + expect(checkIsOpenShift).to.equal(isOpenShift); + }); + + suite('login/logout', function() { + let token: string; + + suiteSetup(async function() { + if (isOpenShift) { + // get current user token and logout + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + token = await Oc.Instance.getCurrentUserToken(); + await Oc.Instance.logout(); + } else { + this.skip(); + } + }); + + teardown(async function() { + // start each test case logged out + try { + await Oc.Instance.logout(); + } catch (e) { + // do nothing, probably already logged out + } + }); + + suiteTeardown(async function() { + // log back in for the rest of the tests + if (isOpenShift) { + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + }); + + test('logout()', async function() { + try { + await Oc.Instance.getCurrentUser(); + expect.fail('should be unable to get current user, since you are logged out'); + } catch (_e) { + // do nothing + } + }); + + test('loginWithUsernamePassword()', async function () { + await Oc.Instance.loginWithUsernamePassword( + clusterUrl, + username, + password, + ); + const currentUser = await Oc.Instance.getCurrentUser(); + expect(currentUser).to.equal(username); + }); + + test('loginWithToken()', async function() { + await Oc.Instance.loginWithToken(clusterUrl, token); + const currentUser = await Oc.Instance.getCurrentUser(); + expect(currentUser).to.equal(username); + }); + + }); + + // TODO: Context modification + // we will need to figure out how to use a temporary `~/.kube/config` + suite('context modification', function() { + + test('setContext()', async function () { + const kc = new KubeConfig(); + kc.loadFromDefault(); + await Oc.Instance.setContext(kc.currentContext); + }); + + test('setContext()'); + test('deleteContext()'); + test('deleteUser()'); + test('deleteCluster()'); + + }); + +}); diff --git a/test/integration/odo.test.ts b/test/integration/odo.test.ts deleted file mode 100644 index 87b46a4fb..000000000 --- a/test/integration/odo.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import { V1Deployment } from '@kubernetes/client-node'; -import { expect } from 'chai'; -import * as fs from 'fs/promises'; -import * as JSYAML from 'js-yaml'; -import { suite, suiteSetup } from 'mocha'; -import * as tmp from 'tmp'; -import { promisify } from 'util'; -import { Uri, workspace } from 'vscode'; -import * as Odo from '../../src/odo'; -import { Command } from '../../src/odo/command'; -import { Project } from '../../src/odo/project'; - -suite('odo integration', function () { - const isOpenShift = process.env.IS_OPENSHIFT || false; - const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; - const username = process.env.CLUSTER_USER || 'developer'; - const password = process.env.CLUSTER_PASSWORD || 'developer'; - - const odo: Odo.Odo = Odo.getInstance(); - - suiteSetup(async function () { - if (isOpenShift) { - try { - await odo.execute(Command.odoLogout()); - } catch (e) { - // do nothing - } - await odo.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - }); - - suiteTeardown(async function () { - // ensure projects are cleaned up - try { - await odo.execute(Command.deleteProject('my-test-project-1')); - } catch (e) { - // do nothing - } - try { - await odo.execute(Command.deleteProject('my-test-project-2')); - } catch (e) { - // do nothing - } - - if (isOpenShift) { - await odo.execute(Command.odoLogout()); - } - }); - - suite('clusters', function () { - test('getActiveCluster()', async function () { - const activeCluster = await odo.getActiveCluster(); - expect(activeCluster).to.equal(clusterUrl); - }); - }); - - suite('projects', function () { - const project1 = 'my-test-project-1'; - const project2 = 'my-test-project-2'; - - suiteSetup(async function () { - try { - await odo.execute(Command.deleteProject(project1)); - } catch (e) { - // do nothing - } - try { - await odo.execute(Command.deleteProject(project2)); - } catch (e) { - // do nothing - } - await odo.createProject(project1); - await odo.createProject(project2); - }); - - suiteTeardown(async function () { - await odo.deleteProject(project1); - await odo.deleteProject(project2); - }); - - test('getProjects()', async function () { - const projects: Project[] = await odo.getProjects(); - expect(projects).length.to.be.greaterThanOrEqual(2); - const projectNames = projects.map((project) => project.name); - expect(projectNames).to.contain(project1); - expect(projectNames).to.contain(project2); - }); - - test('getActiveProject()', async function () { - const activeProject = await odo.getActiveProject(); - // creating a project switches to it, so we expect the active project to be the last one we created, project2 - expect(activeProject).to.equal(project2); - }); - - test('deleteProject()', async function () { - const project3 = 'my-test-project-3'; - await odo.createProject(project3); - await odo.deleteProject(project3); - const projects = await odo.getProjects(); - const projectNames = projects.map((project) => project.name); - expect(projectNames).to.not.contain(project3); - }); - }); - - suite('components', function () { - const project1 = 'my-test-project-1'; - - let tmpFolder1: Uri; - let tmpFolder2: Uri; - - suiteSetup(async function () { - await odo.createProject(project1); - tmpFolder1 = Uri.parse(await promisify(tmp.dir)()); - tmpFolder2 = Uri.parse(await promisify(tmp.dir)()); - await odo.createComponentFromFolder( - 'nodejs', - undefined, - 'component1', - tmpFolder1, - 'nodejs-starter', - ); - await odo.createComponentFromFolder( - 'go', - undefined, - 'component2', - tmpFolder2, - 'go-starter', - ); - }); - - suiteTeardown(async function () { - if (isOpenShift) { - await odo.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - const newWorkspaceFolders = workspace.workspaceFolders.filter((workspaceFolder) => { - const fsPath = workspaceFolder.uri.fsPath; - return (fsPath !== tmpFolder1.fsPath && fsPath !== tmpFolder2.fsPath); - }); - workspace.updateWorkspaceFolders(0, workspace.workspaceFolders.length, ...newWorkspaceFolders); - await fs.rm(tmpFolder1.fsPath, { force: true, recursive: true }); - await fs.rm(tmpFolder2.fsPath, { force: true, recursive: true }); - await odo.deleteProject(project1); - }); - - test('describeComponent()', async function () { - const componentDescription1 = await odo.describeComponent(tmpFolder1.fsPath); - expect(componentDescription1).to.exist; - expect(componentDescription1.managedBy).to.equal('odo'); - const componentDescription2 = await odo.describeComponent(tmpFolder2.fsPath); - expect(componentDescription2).to.exist; - expect(componentDescription2.managedBy).to.equal('odo'); - }); - - test('analyze()', async function () { - const analysis1 = await odo.analyze(tmpFolder1.fsPath); - expect(analysis1).to.exist; - expect(analysis1[0].devfile).to.equal('nodejs'); - const analysis2 = await odo.analyze(tmpFolder2.fsPath); - expect(analysis2).to.exist; - expect(analysis2[0].devfile).to.equal('go'); - }); - - test('canCreatePod()', async function () { - const canCreatePod1 = await odo.canCreatePod(); - expect(canCreatePod1).to.exist; - expect(canCreatePod1).to.equal(true); - if (isOpenShift) { - await odo.execute(Command.odoLogout()); - const canCreatePod2 = await odo.canCreatePod(); - expect(canCreatePod2).to.exist; - expect(canCreatePod2).to.equal(false); - } - }); - }); - - suite('services', function () { - const serviceName = 'my-test-service'; - const projectName = 'my-test-service-project2'; - - // taken from https://docs.openshift.com/container-platform/3.11/dev_guide/deployments/kubernetes_deployments.html - const serviceFileYaml = // - 'apiVersion: apps/v1\n' + // - 'kind: Deployment\n' + // - 'metadata:\n' + // - ` name: ${serviceName}\n` + // - 'spec:\n' + // - ' replicas: 1\n' + // - ' selector:\n' + // - ' matchLabels:\n' + // - ' app: hello-openshift\n' + // - ' template:\n' + // - ' metadata:\n' + // - ' labels:\n' + // - ' app: hello-openshift\n' + // - ' spec:\n' + // - ' containers:\n' + // - ' - name: hello-openshift\n' + // - ' image: openshift/hello-openshift:latest\n' + // - ' ports:\n' + // - ' - containerPort: 80\n'; - - suiteSetup(async function () { - try { - await odo.execute(Command.deleteProject(projectName)); - } catch (e) { - // do nothing - } - await odo.createProject(projectName); - await odo.execute(Command.setNamespace(projectName)); - }); - - suiteTeardown(function () { - // this call fails to exit on minikube/kind - void odo.deleteProject(projectName); - }); - - test('createService()', async function () { - await odo.createService(JSYAML.load(serviceFileYaml)); - const deploymentsOutput = await odo.execute(Command.getDeployments(projectName)); - const deployments: V1Deployment[] = JSON.parse(deploymentsOutput.stdout).items; - const deploymentNames = deployments.map((deployment) => deployment.metadata.name); - expect(deploymentNames).to.contain(serviceName); - }); - - test('loadItems()', async function () { - const deploymentsOutput = await odo.execute(Command.getDeployments(projectName)); - const deployments = odo.loadItems( - deploymentsOutput, - (data) => data.items, - ); - const deploymentNames = deployments.map((deployment) => deployment.metadata.name); - expect(deploymentNames).to.contain(serviceName); - }); - }); - - suite('registries', function () { - const TEST_REGISTRY = 'TestRegistry'; - - suiteSetup(async function () { - const registries = await odo.getRegistries(); - if (registries.find((registry) => registry.name === TEST_REGISTRY)) { - await odo.removeRegistry(TEST_REGISTRY); - } - }); - - suiteTeardown(async function () { - try { - await odo.execute(Command.removeRegistry(TEST_REGISTRY)); - } catch (e) { - // do nothing, it's probably already deleted - } - }); - - test('getRegistries()', async function () { - const registries = await odo.getRegistries(); - expect(registries).to.be.of.length(1); - const registryNames = registries.map((registry) => registry.name); - expect(registryNames).to.contain('DefaultDevfileRegistry'); - }); - - test('addRegistry()', async function () { - await odo.addRegistry(TEST_REGISTRY, 'https://example.org', undefined); - const registries = await odo.getRegistries(); - expect(registries).to.be.of.length(2); - const registryNames = registries.map((registry) => registry.name); - expect(registryNames).to.contain(TEST_REGISTRY); - }); - - test('removeRegistry()', async function () { - await odo.removeRegistry(TEST_REGISTRY); - const registries = await odo.getRegistries(); - expect(registries).to.be.of.length(1); - const registryNames = registries.map((registry) => registry.name); - expect(registryNames).to.not.contain(TEST_REGISTRY); - }); - }); - - suite('require login', function () { - suiteSetup(function () { - if (!isOpenShift) { - this.skip(); - } - }); - - suiteTeardown(async function () { - if (isOpenShift) { - await odo.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - }); - - test('requireLogin()', async function () { - expect(await odo.requireLogin()).to.be.false; - await odo.execute(Command.odoLogout()); - expect(await odo.requireLogin()).to.be.true; - }); - }); - - suite('component types (devfiles)', function () { - test('getComponentTypes()', async function () { - const componentTypes = await odo.getComponentTypes(); - expect(componentTypes).to.not.be.empty; - }); - - test('getCompTypesJson()', async function () { - const componentTypes = await odo.getCompTypesJson(); - expect(componentTypes).to.not.be.empty; - }); - }); - -}); diff --git a/test/integration/odo3.test.ts b/test/integration/odo3.test.ts deleted file mode 100644 index 16413ec4b..000000000 --- a/test/integration/odo3.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import { fail } from 'assert'; -import { expect } from 'chai'; -import * as fs from 'fs/promises'; -import * as tmp from 'tmp'; -import { promisify } from 'util'; -import { Uri, workspace } from 'vscode'; -import * as Odo from '../../src/odo'; -import { Command } from '../../src/odo/command'; -import * as Odo3 from '../../src/odo3'; - -suite('odo 3 integration', function() { - - const isOpenShift = process.env.IS_OPENSHIFT || false; - const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; - const username = process.env.CLUSTER_USER || 'developer'; - const password = process.env.CLUSTER_PASSWORD || 'developer'; - - const odo = Odo.getInstance(); - const odo3 = Odo3.newInstance(); - - const project1 = 'myproject1'; - const project2 = 'myproject2'; - - setup(async function() { - if (isOpenShift) { - await odo.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - await odo.createProject(project1); - await odo.createProject(project2); - }); - - teardown(async function() { - await odo.deleteProject(project1); - await odo.deleteProject(project2); - if (isOpenShift) { - await odo.execute(Command.odoLogout()); - } - }); - - test('get and set namespaces', async function () { - const namespaces = await odo3.getNamespaces(); - const namespaceNames = namespaces.map(kObj => kObj.metadata.name); - expect(namespaceNames).to.contain('myproject1'); - expect(namespaceNames).to.contain('myproject2'); - await odo3.setNamespace('myproject2'); - const activeNamespace = await odo.getActiveProject(); - expect(activeNamespace).to.contain('myproject2'); - }); - - suite('service binding', function() { - let componentFolder: string; - - setup(async function() { - componentFolder = await promisify(tmp.dir)(); - await odo.createComponentFromFolder( - 'nodejs', - undefined, - 'component1', - Uri.parse(componentFolder), - 'nodejs-starter', - ); - }); - - teardown(async function() { - const newWorkspaceFolders = workspace.workspaceFolders.filter((workspaceFolder) => { - const fsPath = workspaceFolder.uri.fsPath; - return (fsPath !== componentFolder); - }); - workspace.updateWorkspaceFolders(0, workspace.workspaceFolders.length, ...newWorkspaceFolders); - await fs.rm(componentFolder, { recursive: true, force: true }); - }); - - test('getBindableServices()', async function() { - const bindableServices = await odo3.getBindableServices(); - expect(bindableServices).to.be.empty; - }); - - test('addBinding()', async function() { - try { - await odo3.addBinding(componentFolder, 'myservice', 'default', 'myservice-binding'); - fail('creating a binding should have failed, since no bindable services are present'); - } catch (_e: unknown) { - // do nothing - } - }); - }); - -}); diff --git a/test/integration/odoWrapper.test.ts b/test/integration/odoWrapper.test.ts new file mode 100644 index 000000000..57eaeef1e --- /dev/null +++ b/test/integration/odoWrapper.test.ts @@ -0,0 +1,359 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { fail } from 'assert'; +import { expect } from 'chai'; +import * as fs from 'fs/promises'; +import { suite, suiteSetup } from 'mocha'; +import * as path from 'path'; +import { which } from 'shelljs'; +import * as tmp from 'tmp'; +import { promisify } from 'util'; +import { Uri, workspace } from 'vscode'; +import { Oc } from '../../src/oc/ocWrapper'; +import { Odo } from '../../src/odo/odoWrapper'; +import { Project } from '../../src/odo/project'; + +suite('./odo/odoWrapper.ts', function () { + const isOpenShift: boolean = Boolean(process.env.IS_OPENSHIFT) || false; + const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; + const username = process.env.CLUSTER_USER || 'developer'; + const password = process.env.CLUSTER_PASSWORD || 'developer'; + + suiteSetup(async function () { + if (isOpenShift) { + try { + await Oc.Instance.logout(); + } catch (e) { + // do nothing + } + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + }); + + suiteTeardown(async function () { + // ensure projects are cleaned up + try { + await Odo.Instance.deleteProject('my-test-project-1'); + } catch (e) { + // do nothing + } + try { + await Odo.Instance.deleteProject('my-test-project-2'); + } catch (e) { + // do nothing + } + + if (isOpenShift) { + await Oc.Instance.logout(); + } + }); + + suite('clusters', function () { + test('getActiveCluster()', async function () { + const activeCluster = await Odo.Instance.getActiveCluster(); + expect(activeCluster).to.equal(clusterUrl); + }); + }); + + suite('projects', function () { + const project1 = 'my-test-project-1'; + const project2 = 'my-test-project-2'; + + suiteSetup(async function () { + try { + await Odo.Instance.deleteProject(project1); + } catch (e) { + // do nothing + } + try { + await Odo.Instance.deleteProject(project2); + } catch (e) { + // do nothing + } + await Odo.Instance.createProject(project1); + await Odo.Instance.createProject(project2); + }); + + suiteTeardown(async function () { + await Odo.Instance.deleteProject(project1); + await Odo.Instance.deleteProject(project2); + }); + + test('getProjects()', async function () { + const projects: Project[] = await Odo.Instance.getProjects(); + expect(projects).length.to.be.greaterThanOrEqual(2); + const projectNames = projects.map((project) => project.name); + expect(projectNames).to.contain(project1); + expect(projectNames).to.contain(project2); + }); + + test('getActiveProject()', async function () { + const activeProject = await Odo.Instance.getActiveProject(); + // creating a project switches to it, so we expect the active project to be the last one we created, project2 + expect(activeProject).to.equal(project2); + }); + + test('setProject()', async function () { + await Odo.Instance.setProject(project1); + const activeNamespace = await Odo.Instance.getActiveProject(); + expect(activeNamespace).to.contain(project1); + }); + + test('deleteProject()', async function () { + const project3 = 'my-test-project-3'; + await Odo.Instance.createProject(project3); + await Odo.Instance.deleteProject(project3); + const projects = await Odo.Instance.getProjects(); + const projectNames = projects.map((project) => project.name); + expect(projectNames).to.not.contain(project3); + }); + + }); + + suite('components', function () { + const project1 = 'my-test-project-1'; + + let tmpFolder1: Uri; + let tmpFolder2: Uri; + + suiteSetup(async function () { + await Odo.Instance.createProject(project1); + tmpFolder1 = Uri.parse(await promisify(tmp.dir)()); + tmpFolder2 = Uri.parse(await promisify(tmp.dir)()); + await Odo.Instance.createComponentFromFolder( + 'nodejs', + undefined, + 'component1', + tmpFolder1, + 'nodejs-starter', + ); + await Odo.Instance.createComponentFromFolder( + 'go', + undefined, + 'component2', + tmpFolder2, + 'go-starter', + ); + }); + + suiteTeardown(async function () { + if (isOpenShift) { + await Oc.Instance.loginWithUsernamePassword(clusterUrl, username, password); + } + const newWorkspaceFolders = workspace.workspaceFolders.filter((workspaceFolder) => { + const fsPath = workspaceFolder.uri.fsPath; + return (fsPath !== tmpFolder1.fsPath && fsPath !== tmpFolder2.fsPath); + }); + workspace.updateWorkspaceFolders(0, workspace.workspaceFolders.length, ...newWorkspaceFolders); + await fs.rm(tmpFolder1.fsPath, { force: true, recursive: true }); + await fs.rm(tmpFolder2.fsPath, { force: true, recursive: true }); + await Odo.Instance.deleteProject(project1); + }); + + test('describeComponent()', async function () { + const componentDescription1 = await Odo.Instance.describeComponent(tmpFolder1.fsPath); + expect(componentDescription1).to.exist; + expect(componentDescription1.managedBy).to.equal('odo'); + const componentDescription2 = await Odo.Instance.describeComponent(tmpFolder2.fsPath); + expect(componentDescription2).to.exist; + expect(componentDescription2.managedBy).to.equal('odo'); + }); + + test('analyze()', async function () { + const analysis1 = await Odo.Instance.analyze(tmpFolder1.fsPath); + expect(analysis1).to.exist; + expect(analysis1[0].devfile).to.equal('nodejs'); + const analysis2 = await Odo.Instance.analyze(tmpFolder2.fsPath); + expect(analysis2).to.exist; + expect(analysis2[0].devfile).to.equal('go'); + }); + }); + + suite('registries', function () { + const TEST_REGISTRY = 'TestRegistry'; + + suiteSetup(async function () { + const registries = await Odo.Instance.getRegistries(); + if (registries.find((registry) => registry.name === TEST_REGISTRY)) { + await Odo.Instance.removeRegistry(TEST_REGISTRY); + } + }); + + suiteTeardown(async function () { + try { + await Odo.Instance.removeRegistry(TEST_REGISTRY); + } catch (e) { + // do nothing, it's probably already deleted + } + }); + + test('getRegistries()', async function () { + const registries = await Odo.Instance.getRegistries(); + expect(registries).to.be.of.length(1); + const registryNames = registries.map((registry) => registry.name); + expect(registryNames).to.contain('DefaultDevfileRegistry'); + }); + + test('addRegistry()', async function () { + await Odo.Instance.addRegistry(TEST_REGISTRY, 'https://example.org', undefined); + const registries = await Odo.Instance.getRegistries(); + expect(registries).to.be.of.length(2); + const registryNames = registries.map((registry) => registry.name); + expect(registryNames).to.contain(TEST_REGISTRY); + }); + + test('removeRegistry()', async function () { + await Odo.Instance.removeRegistry(TEST_REGISTRY); + const registries = await Odo.Instance.getRegistries(); + expect(registries).to.be.of.length(1); + const registryNames = registries.map((registry) => registry.name); + expect(registryNames).to.not.contain(TEST_REGISTRY); + }); + }); + + test('getComponentTypes()', async function () { + const componentTypes = await Odo.Instance.getComponentTypes(); + // TODO: improve + expect(componentTypes).to.not.be.empty; + }); + + test('getDetailedComponentInformation()', async function() { + const componentTypes = await Odo.Instance.getComponentTypes(); + const componentDetails = await Odo.Instance.getDetailedComponentInformation(componentTypes[0]); + // some Devfiles don't have starter projects, but the first Devfile is likely .NET + expect(componentDetails.starterProjects).is.not.empty; + }); + + test('isPodmanPresent()', async function() { + const isPodmanPresent = await Odo.Instance.isPodmanPresent(); + const whichImplementationPodmanPresent = which('podman') !== null; + expect(isPodmanPresent).to.equal(whichImplementationPodmanPresent); + }); + + test('getStarterProjects()', async function() { + const starterProjects = await Odo.Instance.getStarterProjects({ + registryName: 'DefaultDevfileRegistry', + description: '.NET 5.0 application', + label: '', + name: 'dotnet50', + version: '1.0.3', + }); + expect(starterProjects).to.have.length(1); + expect(starterProjects[0].name).to.equal('dotnet50-example'); + const udiStarterProjects = await Odo.Instance.getStarterProjects({ + registryName: 'DefaultDevfileRegistry', + description: 'Universal Developer Image provides ', + label: '', + name: 'udi', + version: '1.0.0', + }); + expect(udiStarterProjects).to.have.length(0); + }); + + suite('create component', function() { + + const COMPONENT_TYPE = 'dotnet50'; + + let tmpFolder: string; + + suiteSetup(async function() { + tmpFolder = await promisify(tmp.dir)(); + }); + + suiteTeardown(async function() { + await fs.rm(tmpFolder, { recursive: true, force: true }); + }); + + test('createComponentFromTemplateProject()', async function() { + await Odo.Instance.createComponentFromTemplateProject(tmpFolder, 'my-component', 8080, COMPONENT_TYPE, 'DefaultDevfileRegistry', 'dotnet50-example'); + try { + await fs.access(path.join(tmpFolder, 'devfile.yaml')); + } catch (_) { + fail('Expected devfile to be created'); + } + }); + + test('analyze()', async function() { + const [analysis] = await Odo.Instance.analyze(tmpFolder); + expect(analysis.name).to.equal(path.basename(tmpFolder).toLocaleLowerCase()); + expect(analysis.devfile).to.equal(COMPONENT_TYPE); + }); + + test('deleteComponentConfiguration()', async function() { + await Odo.Instance.deleteComponentConfiguration(tmpFolder); + try { + await fs.access(path.join(tmpFolder, 'devfile.yaml')); + fail('devfile.yaml should have been deleted') + } catch (_e) { + // deleted successfully + } + }); + + test('createComponentFromLocation()', async function() { + // the project already exists from the previous step, + // we just need to recreate the Devfile + await Odo.Instance.createComponentFromLocation('dotnet50', 'my-component', 8080, Uri.file(tmpFolder)); + try { + await fs.access(path.join(tmpFolder, 'devfile.yaml')); + } catch (_) { + fail('Expected devfile to be created'); + } + }); + + test('addBinding()', async function() { + try { + await Odo.Instance.addBinding(tmpFolder, 'default', 'my-service', 'my-service-binding'); + // TODO: set up a service to bind to, + // for now check that the correct error message appears + fail('The service doesn\'t exist, so binding should have failed'); + } catch (e) { + expect(`${e}`).to.contain('No bindable service instances found in namespace'); + } + }) + + }); + + suite('service binding', function() { + let componentFolder: string; + + setup(async function() { + componentFolder = await promisify(tmp.dir)(); + await Odo.Instance.createComponentFromFolder( + 'nodejs', + undefined, + 'component1', + Uri.parse(componentFolder), + 'nodejs-starter', + ); + }); + + teardown(async function() { + const newWorkspaceFolders = workspace.workspaceFolders.filter((workspaceFolder) => { + const fsPath = workspaceFolder.uri.fsPath; + return (fsPath !== componentFolder); + }); + workspace.updateWorkspaceFolders(0, workspace.workspaceFolders.length, ...newWorkspaceFolders); + await fs.rm(componentFolder, { recursive: true, force: true }); + }); + + test('getBindableServices()', async function() { + const bindableServices = await Odo.Instance.getBindableServices(); + expect(bindableServices).to.be.empty; + }); + + test('addBinding()', async function() { + try { + await Odo.Instance.addBinding(componentFolder, 'myservice', 'default', 'myservice-binding'); + fail('creating a binding should have failed, since no bindable services are present'); + } catch (_e: unknown) { + // do nothing + } + }); + }); + + test('deleteComponentConfiguration'); + +}); diff --git a/test/integration/project.test.ts b/test/integration/project.test.ts deleted file mode 100644 index 67f7820af..000000000 --- a/test/integration/project.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import { expect } from 'chai'; -import { getInstance } from '../../src/odo'; -import { Command } from '../../src/odo/command'; -import { Command as ProjectCommand } from '../../src/openshift/project'; - -suite('openshift/project.ts', function () { - - const isOpenShift = process.env.IS_OPENSHIFT || false; - const clusterUrl = process.env.CLUSTER_URL || 'https://api.crc.testing:6443'; - const username = process.env.CLUSTER_USER || 'developer'; - const password = process.env.CLUSTER_PASSWORD || 'developer'; - - const TEST_PROJECT_1 = 'test-project-1'; - const TEST_PROJECT_2 = 'test-project-2'; - - const ODO = getInstance(); - - suiteSetup(async function () { - if (isOpenShift && await ODO.requireLogin()) { - await ODO.execute(Command.odoLoginWithUsernamePassword(clusterUrl, username, password)); - } - try { - await ODO.deleteProject(TEST_PROJECT_1); - } catch (e) { - // do nothing - } - try { - await ODO.deleteProject(TEST_PROJECT_2); - } catch (e) { - // do nothing - } - await ODO.createProject(TEST_PROJECT_1); - await ODO.createProject(TEST_PROJECT_2); - }); - - suiteTeardown(async function () { - try { - await ODO.deleteProject(TEST_PROJECT_1); - } catch (e) { - // do nothing - } - try { - await ODO.deleteProject(TEST_PROJECT_2); - } catch (e) { - // do nothing - } - }); - - test('Command.setActiveProject()', async function () { - await ODO.execute(ProjectCommand.setActiveProject(TEST_PROJECT_2)); - let activeProject = await ODO.getActiveProject(); - expect(activeProject).to.equal(TEST_PROJECT_2); - - await ODO.execute(ProjectCommand.setActiveProject(TEST_PROJECT_1)); - activeProject = await ODO.getActiveProject(); - expect(activeProject).to.equal(TEST_PROJECT_1); - }); - - test('Command.getAll()', async function() { - const res = await ODO.execute(ProjectCommand.getAll(TEST_PROJECT_1)); - expect(JSON.parse(res.stdout).items).to.have.length(0); - }); - - test('Command.deleteProject()', async function() { - await ODO.execute(ProjectCommand.deleteProject(TEST_PROJECT_1)); - const projects = await ODO.getProjects(); - expect(projects).not.to.contain(TEST_PROJECT_1); - }); - -}); diff --git a/test/unit/debug.test.ts b/test/unit/debug.test.ts index f63466ba1..527584438 100644 --- a/test/unit/debug.test.ts +++ b/test/unit/debug.test.ts @@ -5,10 +5,8 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; -import { DebugSession, Disposable, TreeItem, debug } from 'vscode'; +import { DebugSession, Disposable, debug } from 'vscode'; import { DebugSessionsView } from '../../src/debug'; -import { ContextType } from '../../src/odo'; -import { TestItem } from './openshift/testOSItem'; import sinon = require('sinon'); const {expect} = chai; @@ -20,10 +18,6 @@ suite('Debug Sessions View', () => { let view: DebugSessionsView; let startEmitter: (session:DebugSession)=> void; let stopEmitter: (session:DebugSession)=> void; - const clusterItem = new TestItem(null, 'cluster', ContextType.CLUSTER); - const projectItem = new TestItem(clusterItem, 'project', ContextType.PROJECT); - const appItem = new TestItem(projectItem, 'application', ContextType.APPLICATION); - const componentItem = new TestItem(appItem, 'component', ContextType.COMPONENT); const debugSession: any = { id: 'unique', name: 'name', @@ -63,7 +57,6 @@ suite('Debug Sessions View', () => { const children = await view.getChildren(); expect(children.length).equals(1); expect(view.getParent()).undefined; - expect((view.getTreeItem(children[0]) as TreeItem).label).includes(componentItem.label); }); test('removes component from view after debugger disconnect command executed', async () => { diff --git a/test/unit/explorer.test.ts b/test/unit/explorer.test.ts index f357c678b..d873b4861 100644 --- a/test/unit/explorer.test.ts +++ b/test/unit/explorer.test.ts @@ -7,7 +7,7 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import { commands, Uri } from 'vscode'; import { OpenShiftExplorer } from '../../src/explorer'; -import { OdoImpl } from '../../src/odo'; +import { Odo } from '../../src/odo/odoWrapper'; import sinon = require('sinon'); const {expect} = chai; @@ -17,7 +17,7 @@ suite('OpenShift Application Explorer', () => { const sandbox = sinon.createSandbox(); setup(() => { - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves('cluster'); + sandbox.stub(Odo.prototype, 'getActiveCluster').resolves('cluster'); }); teardown(() => { diff --git a/test/unit/extension.test.ts b/test/unit/extension.test.ts index 378aee5d3..dfcf46dac 100644 --- a/test/unit/extension.test.ts +++ b/test/unit/extension.test.ts @@ -14,7 +14,8 @@ import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; import { CommandText } from '../../src/base/command'; -import { OdoImpl } from '../../src/odo'; +import { Oc } from '../../src/oc/ocWrapper'; +import { Odo } from '../../src/odo/odoWrapper'; import { Project } from '../../src/odo/project'; import { Progress } from '../../src/util/progress'; import path = require('path'); @@ -66,7 +67,7 @@ suite('openshift toolkit Extension', () => { uri: comp2Uri, index: 1, name: 'comp2' }]); // eslint-disable-next-line @typescript-eslint/require-await - sandbox.stub(OdoImpl.prototype, 'execute').callsFake(async (cmd: CommandText, cwd: string)=> { + sandbox.stub(Odo.prototype, 'execute').callsFake(async (cmd: CommandText, cwd: string)=> { if (`${cmd}`.includes('version')) { return { error: undefined, stdout: 'Server: https://api.crc.testing:6443', stderr: '' }; } @@ -86,17 +87,11 @@ suite('openshift toolkit Extension', () => { "devfileComponents": [] }`, stderr: ''} } - if (`${cmd}`.includes('all')) { - return { - error: undefined, - stdout: '{ "items": [] }', - stderr: '' - }; - } return { error: undefined, stdout: '', stderr: ''}; }); - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves('cluster'); - sandbox.stub(OdoImpl.prototype, 'getProjects').resolves([projectItem]); + sandbox.stub(Oc.prototype, 'getAllKubernetesObjects').resolves([]); + sandbox.stub(Odo.prototype, 'getActiveCluster').resolves('cluster'); + sandbox.stub(Odo.prototype, 'getProjects').resolves([projectItem]); }); teardown(() => { diff --git a/test/unit/index.ts b/test/unit/index.ts index a21ba2754..ae4246561 100644 --- a/test/unit/index.ts +++ b/test/unit/index.ts @@ -60,6 +60,7 @@ export async function run(): Promise { testFiles.push(...await testFinder('**/extension.test.js')); testFiles.push(...await testFinder('**/workspace.test.js')); testFiles.push(...await testFinder('openshift/component.test.js')); + testFiles.push(...await testFinder('openshift/cluster.test.js')); testFiles.push(...await testFinder('k8s/*.test.js')); testFiles.push(...await testFinder('util/*.test.js')); diff --git a/test/unit/oc.test.ts b/test/unit/oc.test.ts index 8768c226e..af76509a5 100644 --- a/test/unit/oc.test.ts +++ b/test/unit/oc.test.ts @@ -7,11 +7,12 @@ import * as chai from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { window } from 'vscode'; -import { Oc } from '../../src/oc'; -import { getInstance } from '../../src/odo'; +import { Oc } from '../../src/oc/ocWrapper'; +import { Odo } from '../../src/odo/odoWrapper'; import { Project } from '../../src/odo/project'; import { ToolsConfig } from '../../src/tools'; import { ChildProcessUtil } from '../../src/util/childProcessUtil'; +import { YamlFileCommands } from '../../src/yamlFileCommands'; const {expect} = chai; chai.use(sinonChai); @@ -44,11 +45,11 @@ suite('Oc', function() { sandbox = sinon.createSandbox(); warnStub = sandbox.stub(window, 'showWarningMessage'); execStub = sandbox.stub(ChildProcessUtil.prototype, 'execute'); - getActiveProjectStub = sandbox.stub(getInstance(), 'getActiveProject').resolves('my-project'); + getActiveProjectStub = sandbox.stub(Odo.Instance, 'getActiveProject').resolves('my-project'); detectOrDownloadStub = sandbox.stub(ToolsConfig, 'detect').resolves('path'); - sandbox.stub(getInstance(), 'getActiveCluster').resolves('cluster'); - sandbox.stub(getInstance(), 'getProjects').resolves([projectItem]); - sandbox.stub(getInstance(), 'canCreatePod').resolves(true); + sandbox.stub(Odo.Instance, 'getActiveCluster').resolves('cluster'); + sandbox.stub(Odo.Instance, 'getProjects').resolves([projectItem]); + sandbox.stub(Oc.Instance, 'canCreatePod').resolves(true); }); teardown(function() { @@ -56,7 +57,7 @@ suite('Oc', function() { }); test('show warning message if file is not json or yaml', async function() { - await Oc.create(); + await YamlFileCommands.create(); expect(warnStub).is.calledOnce; }); @@ -68,18 +69,7 @@ suite('Oc', function() { }, }); detectOrDownloadStub.onFirstCall().resolves(undefined); - await Oc.create(); - expect(warnStub).is.calledOnce; - }); - - test('show warning message if oc command not found', async function() { - sandbox.stub(window, 'activeTextEditor').value({ - document: { - fileName: 'manifests.yaml', - }, - }); - detectOrDownloadStub.onFirstCall().resolves(undefined); - await Oc.create(); + await YamlFileCommands.create(); expect(warnStub).is.calledOnce; }); @@ -97,7 +87,7 @@ suite('Oc', function() { save: sinon.stub().returns(true) }, }); - const result = await Oc.create(); + const result = await YamlFileCommands.create(); expect(result).equals('Resources were successfully created.'); }); @@ -109,7 +99,7 @@ suite('Oc', function() { isDirty: true, }, }); - await Oc.create(); + await YamlFileCommands.create(); expect(warnStub).is.calledOnce; expect(infoMsg).is.calledOnce; }); @@ -121,7 +111,7 @@ suite('Oc', function() { stdout: 'imagestream.image.openshift.io/spring-petclinic created\ndeploymentconfig.apps.openshift.io/spring-petclinic created' }); sandbox.stub(window, 'activeTextEditor').value(TextEditorMock); - const result = await Oc.create(); + const result = await YamlFileCommands.create(); expect(result).equals('Resources were successfully created.'); }); @@ -130,7 +120,7 @@ suite('Oc', function() { execStub.rejects('error'); sandbox.stub(window, 'activeTextEditor').value(TextEditorMock); try { - await Oc.create(); + await YamlFileCommands.create(); } catch (err) { savedErr = err; } @@ -141,7 +131,7 @@ suite('Oc', function() { getActiveProjectStub.resetBehavior(); getActiveProjectStub.resolves(undefined); sandbox.stub(window, 'activeTextEditor').value(TextEditorMock); - expect(await Oc.create()).null; + expect(await YamlFileCommands.create()).null; }); }); diff --git a/test/unit/odo/workspace.test.ts b/test/unit/odo/workspace.test.ts index 15ab4f51c..49f5764b1 100644 --- a/test/unit/odo/workspace.test.ts +++ b/test/unit/odo/workspace.test.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import * as chai from 'chai'; -import * as sinonChai from 'sinon-chai'; import * as sinon from 'sinon'; -import * as fixtures from '../../fixtures'; +import * as sinonChai from 'sinon-chai'; +import * as vscode from 'vscode'; +import { ComponentDescription } from '../../../src/odo/componentTypeDescription'; +import { Odo } from '../../../src/odo/odoWrapper'; import { OdoWorkspace } from '../../../src/odo/workspace'; -import { OdoImpl } from '../../../src/odo'; +import * as fixtures from '../../fixtures'; import path = require('path'); -import { ComponentDescription } from '../../../src/odo/componentTypeDescription'; -import { Odo3Impl } from '../../../src/odo3'; const {expect} = chai; chai.use(sinonChai); @@ -28,7 +27,7 @@ suite('Odo/Workspace', () => { }); function stubDescribeComponentCall() { - return sandbox.stub(Odo3Impl.prototype, 'describeComponent').callsFake((contextPath: string): Promise => { + return sandbox.stub(Odo.Instance, 'describeComponent').callsFake((contextPath: string): Promise => { if (contextPath.includes(`${path.sep}comp`, contextPath.lastIndexOf(path.sep))) { return Promise.resolve({} as ComponentDescription); } @@ -78,7 +77,7 @@ suite('Odo/Workspace', () => { const changeWorkspaceFolders = new vscode.EventEmitter(); sandbox.stub(vscode.workspace, 'onDidChangeWorkspaceFolders').value(changeWorkspaceFolders.event); const onDidChangeComponentsStub = sandbox.stub(OdoWorkspace.prototype, 'onDidChangeComponents'); - sandbox.stub(OdoImpl.prototype, 'describeComponent').callsFake((contextPath: string): Promise => { + sandbox.stub(Odo.prototype, 'describeComponent').callsFake((contextPath: string): Promise => { if (contextPath.includes('/comp', contextPath.lastIndexOf(path.sep))) { return Promise.resolve({} as ComponentDescription); } diff --git a/test/unit/odo.test.ts b/test/unit/odoWrapper.test.ts similarity index 89% rename from test/unit/odo.test.ts rename to test/unit/odoWrapper.test.ts index 1f7111362..f34eb2f2d 100644 --- a/test/unit/odo.test.ts +++ b/test/unit/odoWrapper.test.ts @@ -11,15 +11,15 @@ import * as sinonChai from 'sinon-chai'; import { window, workspace } from 'vscode'; import { CommandText } from '../../src/base/command'; import { CliChannel } from '../../src/cli'; -import * as odo from '../../src/odo'; +import { Odo } from '../../src/odo/odoWrapper'; import { ToolsConfig } from '../../src/tools'; import { ChildProcessUtil, CliExitData } from '../../src/util/childProcessUtil'; const {expect} = chai; chai.use(sinonChai); -suite('odo', () => { - const odoCli: odo.Odo = odo.OdoImpl.Instance; +suite('./odo/odoWrapper.ts', () => { + const odoCli = Odo.Instance; let sandbox: sinon.SinonSandbox; const errorMessage = 'Error'; @@ -204,7 +204,7 @@ suite('odo', () => { }; setup(() => { - sandbox.stub(odo.OdoImpl.prototype, 'execute').resolves(componentCatalog); + sandbox.stub(Odo.prototype, 'execute').resolves(componentCatalog); }); }); @@ -239,7 +239,7 @@ suite('odo', () => { }; setup(() => { - sandbox.stub(odo.OdoImpl.prototype, 'execute').resolves(data); + sandbox.stub(Odo.prototype, 'execute').resolves(data); }); }); @@ -254,28 +254,14 @@ suite('odo', () => { ]; test('extension uses odo version to get cluster url', async () => { - sandbox.stub(odo.OdoImpl.prototype, 'execute').resolves({ + sandbox.stub(Odo.prototype, 'execute').resolves({ error: undefined, stdout: odoVersionOutLoggedIn.join('\n'), stderr: '' }); - const cluster: string = await odo.getInstance().getActiveCluster(); + const cluster: string = await odoCli.getActiveCluster(); expect(cluster).equals(clusterUrl); }); - test('extension uses odo version to determine if login is required', async () => { - const stub = sandbox.stub(odoCli, 'execute').resolves({ error: null, stdout: 'logged in', stderr: ''}); - const result = await odoCli.requireLogin(); - - expect(stub).calledWith(new CommandText('oc', 'whoami')); - expect(result).false; - }); - - test('requireLogin returns true if odo is not logged in to the cluster', async () => { - sandbox.stub(odoCli, 'execute').returns(Promise.reject('Not logged in!')); - const result = await odoCli.requireLogin(); - - expect(result).true; - }); }); }); diff --git a/test/unit/openshift/cluster.test.ts b/test/unit/openshift/cluster.test.ts index 3cce319eb..c367ece03 100644 --- a/test/unit/openshift/cluster.test.ts +++ b/test/unit/openshift/cluster.test.ts @@ -7,27 +7,31 @@ import * as chai from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; +import { CliChannel } from '../../../src/cli'; import { OpenShiftExplorer } from '../../../src/explorer'; -import { ContextType, OdoImpl } from '../../../src/odo'; +import { Oc } from '../../../src/oc/ocWrapper'; import { Command } from '../../../src/odo/command'; +import { Odo } from '../../../src/odo/odoWrapper'; import { Cluster } from '../../../src/openshift/cluster'; import { CliExitData } from '../../../src/util/childProcessUtil'; import { TokenStore } from '../../../src/util/credentialManager'; +import { LoginUtil } from '../../../src/util/loginUtil'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; -import { TestItem } from './testOSItem'; -import pq = require('proxyquire'); const {expect} = chai; chai.use(sinonChai); -suite('Openshift/Cluster', () => { +suite('Openshift/Cluster', function() { let sandbox: sinon.SinonSandbox; - let execStub: sinon.SinonStub; - let commandStub: sinon.SinonSpy; - let inputStub: sinon.SinonStub; - let infoStub: sinon.SinonStub; - let loginStub: sinon.SinonStub; - let quickPickStub: sinon.SinonStub; + let execStub: sinon.SinonStub; + let commandStub: sinon.SinonSpy; + let inputStub: sinon.SinonStub; + let infoStub: sinon.SinonStub; + let requireLoginStub: sinon.SinonStub; + let quickPickStub: sinon.SinonStub; + let passwordLoginStub: sinon.SinonStub; + let tokenLoginStub: sinon.SinonStub; + let logoutStub: sinon.SinonStub; const testData: CliExitData = { error: undefined, @@ -53,19 +57,22 @@ suite('Openshift/Cluster', () => { setup(() => { sandbox = sinon.createSandbox(); sandbox.stub(TokenStore.extensionContext.secrets); - execStub = sandbox.stub(OdoImpl.prototype, 'execute').resolves(testData); + execStub = sandbox.stub(Odo.prototype, 'execute').resolves(testData); inputStub = sandbox.stub(vscode.window, 'showInputBox'); commandStub = sandbox.stub(vscode.commands, 'executeCommand').resolves(); infoStub = sandbox.stub(vscode.window, 'showInformationMessage').resolves('Yes'); quickPickStub = sandbox.stub(vscode.window, 'showQuickPick').resolves({label: 'Credentials', description: 'Log in to the given server using credentials'}); - loginStub = sandbox.stub(OdoImpl.prototype, 'requireLogin').resolves(true); + requireLoginStub = sandbox.stub(LoginUtil.prototype, 'requireLogin').resolves(true); + passwordLoginStub = sandbox.stub(Oc.prototype, 'loginWithUsernamePassword').resolves(); + tokenLoginStub = sandbox.stub(Oc.prototype, 'loginWithToken').resolves(); + logoutStub = sandbox.stub(Oc.prototype, 'logout').resolves(); }); teardown(() => { sandbox.restore(); }); - suite('login', () => { + suite.skip('login', () => { setup(() => { quickPickStub.onFirstCall().resolves({description: 'Current Context', label: testUrl}); @@ -74,7 +81,7 @@ suite('Openshift/Cluster', () => { test('exits if login confirmation declined', async () => { infoStub.resolves('No'); - loginStub.resolves(false); + requireLoginStub.resolves(false); const status = await Cluster.login(); expect(status).null; @@ -105,14 +112,14 @@ suite('Openshift/Cluster', () => { }); test('exits if the user refuses to log out of an existing cluster', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); infoStub.resolves('No'); const status = await Cluster.login(); expect(status).null; }); test('exits if the user refuses to select the way to log in to the cluster', async () => { - loginStub.resolves('Yes'); + requireLoginStub.resolves('Yes'); quickPickStub.resolves(undefined); const status = await Cluster.login(); expect(status).null; @@ -125,7 +132,7 @@ suite('Openshift/Cluster', () => { }); test('logins to new cluster if user answer yes to a warning', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); infoStub.resolves('Yes'); const result = await Cluster.credentialsLogin(false, testUrl); expect(result).equals(`Successfully logged in to '${testUrl}'`); @@ -153,14 +160,14 @@ suite('Openshift/Cluster', () => { }); test('exits if the user cancels url input box', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); inputStub.onFirstCall().resolves(null); const result = await Cluster.credentialsLogin(null); expect(result).null; }); test('exits if the user refuses to login to new cluster', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); infoStub.resolves('No'); const result = await Cluster.credentialsLogin(null); expect(result).null; @@ -174,7 +181,7 @@ suite('Openshift/Cluster', () => { const status = await Cluster.credentialsLogin(false, testUrl); expect(status).equals(`Successfully logged in to '${testUrl}'`); - expect(execStub).calledOnceWith(Command.odoLoginWithUsernamePassword(testUrl, testUser, password)); + expect(passwordLoginStub).calledOnceWith(testUrl, testUser, password); expect(commandStub).calledWith('setContext', 'isLoggedIn', true); }); @@ -236,21 +243,21 @@ suite('Openshift/Cluster', () => { }); test('logins to new cluster if user answer yes to a warning', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); infoStub.resolves('Yes'); const result = await Cluster.tokenLogin(testUrl); expect(result).equals(`Successfully logged in to '${testUrl}'`); }); test('exits if the user cancels url input box', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); inputStub.onFirstCall().resolves(null); const result = await Cluster.tokenLogin(testUrl); expect(result).null; }); test('exits if the user refuses to login to new cluster', async () => { - loginStub.resolves(false); + requireLoginStub.resolves(false); infoStub.resolves('No'); const result = await Cluster.tokenLogin(null); expect(result).null; @@ -260,7 +267,7 @@ suite('Openshift/Cluster', () => { const status = await Cluster.tokenLogin(testUrl); expect(status).equals(`Successfully logged in to '${testUrl}'`); - expect(execStub).calledOnceWith(Command.odoLoginWithToken(testUrl, token)); + expect(tokenLoginStub).calledOnceWith(testUrl, token) expect(commandStub).calledWith('setContext', 'isLoggedIn', true); }); @@ -285,7 +292,7 @@ suite('Openshift/Cluster', () => { }); }); - suite('logout', () => { + suite.skip('logout', () => { let warnStub: sinon.SinonStub; setup(() => { @@ -298,7 +305,7 @@ suite('Openshift/Cluster', () => { const status = await Cluster.logout(); expect(status).null; - expect(execStub).calledOnceWith(Command.odoLogout()); + expect(logoutStub).calledOnce; expect(commandStub).calledWith('setContext', 'isLoggedIn', false); }); @@ -382,7 +389,7 @@ suite('Openshift/Cluster', () => { }); }); - suite('switchContext', () => { + suite.skip('switchContext', () => { const choice = { label: 'minishift' }; @@ -403,49 +410,36 @@ suite('Openshift/Cluster', () => { }); }); - suite('open console', () => { - const openStub: sinon.SinonStub = sinon.stub(); - let clusterMock: { openshiftConsole: { (arg0: TestItem): void; (): void; (): void } }; - let cluster: TestItem; + suite('open console', function () { + const CLUSTER_NAME = 'http://localhost:6443'; + const CONSOLE_URL = 'http://localhost:6443/console'; - setup(() => { - clusterMock = pq('../../../src/openshift/cluster', { - open: openStub - }).Cluster; - cluster = new TestItem(null, 'http://localhost', ContextType.CLUSTER); - }); - - test('opens URL from cluster\'s tree item label if called from cluster\'s context menu', () => { - clusterMock.openshiftConsole(cluster); - openStub.calledOnceWith('http://localhost'); - }); + let cliExecStub: sinon.SinonStub; - test('opens URL from first cluster label', () => { - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves(cluster.getName()); - clusterMock.openshiftConsole(); - openStub.calledOnceWith('http://localhost'); + setup(function() { + cliExecStub = sandbox.stub(CliChannel.getInstance(), 'executeTool'); }); - test('shows error message if node label is not URL', () => { - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves(cluster.getName()); - const errMsgStub = sandbox.stub(vscode.window, 'showErrorMessage'); - clusterMock.openshiftConsole(); - errMsgStub.calledOnceWith('localhost', undefined); + test('opens URL on OpenShift cluster', async function() { + cliExecStub.callsFake(() => + ({ stdout: JSON.stringify({data: {consoleURL: CONSOLE_URL}}) }) + ); + await Cluster.openOpenshiftConsole(); + commandStub.calledOnceWith('vscode.open', vscode.Uri.parse(CONSOLE_URL)); }); - test('opens cluster\'s URL from context menu', () => { - execStub.onFirstCall().resolves({ - error: null, - stderr: fatalErrorText, - stdout: 'output' - }); - execStub.onSecondCall().resolves({ - error: null, - stderr: null, - stdout: 'http://localhost' + test('opens URL on non-OpenShift cluster', async function() { + let counter = 0; + cliExecStub.callsFake(() => { + if (counter === 0) { + counter ++; + throw new Error(); + } + return { stdout: CLUSTER_NAME }; }); - clusterMock.openshiftConsole(cluster); - openStub.calledOnceWith('http://localhost'); + await Cluster.openOpenshiftConsole(); + commandStub.calledOnceWith('vscode.open', vscode.Uri.parse(CONSOLE_URL)); }); + }); }); diff --git a/test/unit/openshift/component.test.ts b/test/unit/openshift/component.test.ts index 0a6116973..7d30d1221 100644 --- a/test/unit/openshift/component.test.ts +++ b/test/unit/openshift/component.test.ts @@ -10,17 +10,16 @@ import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; import { ComponentInfo, ComponentsTreeDataProvider } from '../../../src/componentsView'; -import { ContextType, OdoImpl } from '../../../src/odo'; import { Command } from '../../../src/odo/command'; import { ComponentTypeAdapter } from '../../../src/odo/componentType'; import { CommandProvider } from '../../../src/odo/componentTypeDescription'; +import { Odo } from '../../../src/odo/odoWrapper'; import { Project } from '../../../src/odo/project'; import { ComponentWorkspaceFolder, OdoWorkspace } from '../../../src/odo/workspace'; import * as openShiftComponent from '../../../src/openshift/component'; import * as Util from '../../../src/util/async'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; import { comp1Folder } from '../../fixtures'; -import { TestItem } from './testOSItem'; import pq = require('proxyquire'); import fs = require('fs-extra'); @@ -37,7 +36,6 @@ suite('OpenShift/Component', function () { const wsFolder1 = { uri: comp1Uri, index: 0, name: 'comp1' }; const wsFolder2 = { uri: comp2Uri, index: 1, name: 'comp2' }; const projectItem = { name: 'myproject', active: false } as Project; - const componentItem = new TestItem(undefined, 'comp1', ContextType.COMPONENT_PUSHED, [], comp1Uri, 'https://host/proj/app/comp1', 'nodejs'); const componentItem1: ComponentWorkspaceFolder = { contextPath: comp1Folder, component: { @@ -130,10 +128,10 @@ suite('OpenShift/Component', function () { sandbox.stub(vscode.workspace, 'updateWorkspaceFolders'); Component = pq('../../../src/openshift/component', {}).Component; termStub = sandbox.stub(OpenShiftTerminalManager.prototype, 'executeInTerminal'); - execStub = sandbox.stub(OdoImpl.prototype, 'execute').resolves({ stdout: '', stderr: undefined, error: undefined }); - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves('cluster'); - sandbox.stub(OdoImpl.prototype, 'getProjects').resolves([projectItem]); - sandbox.stub(OdoImpl.prototype, 'describeComponent').resolves(componentItem1.component); + execStub = sandbox.stub(Odo.prototype, 'execute').resolves({ stdout: '', stderr: undefined, error: undefined }); + sandbox.stub(Odo.prototype, 'getActiveCluster').resolves('cluster'); + sandbox.stub(Odo.prototype, 'getProjects').resolves([projectItem]); + sandbox.stub(Odo.prototype, 'describeComponent').resolves(componentItem1.component); sandbox.stub(OdoWorkspace.prototype, 'getComponents').resolves([componentItem1]); sandbox.stub(Util, 'wait').resolves(); commandStub = sandbox.stub(vscode.commands, 'executeCommand').resolves(); @@ -152,116 +150,6 @@ suite('OpenShift/Component', function () { }); }); - suite.skip('createFromFolder', () => { - let inputStub: sinon.SinonStub; - const pathOne: string = path.join('some', 'path'); - const folder: vscode.Uri = vscode.Uri.file(pathOne); - const componentType = new ComponentTypeAdapter('nodejs', 'latest', 'builder,nodejs'); - - setup(() => { - quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); - inputStub = sandbox.stub(vscode.window, 'showInputBox'); - }); - - test('returns empty string and step name in cancelled_step property when no option selected from quick pick', async () => { - quickPickStub.onFirstCall().resolves(undefined); - const result = await Component.createFromRootWorkspaceFolder(null, [], {}); - expect(result.toString()).equals(''); - }); - - test('returns empty string and step name in cancelled_step property when no component type selected', async () => { - inputStub.resolves(componentItem.getName()); - quickPickStub.onSecondCall().resolves(null); - const result = await Component.createFromRootWorkspaceFolder(folder, [], {}); - expect(result.toString()).equals(''); - }); - - test('returns empty string and step name in cancelled_step property when no component name is provided', async () => { - inputStub.resolves(); - const result = await Component.createFromRootWorkspaceFolder(folder, [], {}); - expect(result.toString()).equals(''); - }); - - test('happy path works', async () => { - inputStub.resolves(componentItem.getName()); - quickPickStub.onSecondCall().resolves(componentType); - const result = await Component.createFromRootWorkspaceFolder(folder, [], {}); - expect(result.toString()).equals(`Component '${componentItem.getName()}' successfully created. Perform actions on it from Components View.`); - }); - - test('skips component type selection if componentTypeName provided and only one type found in registries', async () => { - inputStub.resolves(componentItem.getName()); - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([ - new ComponentTypeAdapter( - 'componentType1', - undefined, - 'description', - '' - ) - ]); - const result = await Component.createFromRootWorkspaceFolder(folder, [], { componentTypeName: 'componentType1' }); - expect(result.toString()).equals(`Component '${componentItem.getName()}' successfully created. Perform actions on it from Components View.`); - expect(quickPickStub).calledOnce; - expect(quickPickStub).have.not.calledWith({ placeHolder: 'Component type' }) - }); - - test('when componentTypeName provided and there are more than one type found in registries, asks to pick component type from list of found types', async () => { - inputStub.resolves(componentItem.getName()); - const componentType1 = new ComponentTypeAdapter( - 'componentType1', - undefined, - 'description', - '', - 'reg1' - ); - const componentType2 = new ComponentTypeAdapter( - 'componentType1', - undefined, - 'description', - '', - 'reg2' - ); - quickPickStub.onSecondCall().resolves(componentType1) - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([ - componentType1, componentType2 - ]); - const result = await Component.createFromRootWorkspaceFolder(folder, [], { componentTypeName: 'componentType1' }); - expect(result.toString()).equals(`Component '${componentItem.getName()}' successfully created. Perform actions on it from Components View.`); - expect(quickPickStub).calledTwice; - expect(quickPickStub).calledWith([componentType1, componentType2]); - }); - - test('when componentTypeName provided and there is no type found in registries, asks to select from all available registries', async () => { - inputStub.resolves(componentItem.getName()); - const componentType1 = new ComponentTypeAdapter( - 'componentType2', - undefined, - 'description', - '' - ); - quickPickStub.onSecondCall().resolves(componentType1) - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([ - componentType1 - ]); - const result = await Component.createFromRootWorkspaceFolder(folder, [], { componentTypeName: 'componentType1' }); - expect(result.toString()).equals(`Component '${componentItem.getName()}' successfully created. Perform actions on it from Components View.`); - expect(quickPickStub).calledTwice; - expect(quickPickStub).calledWith([componentType1]); - }); - - test('skips component type selection if devfile exists and use devfile name as initial value for component name', async () => { - sandbox.stub(fs, 'existsSync').returns(true); - sandbox.stub(Component, 'getName').resolves(componentItem.getName()); - sandbox.stub(fs, 'readFileSync').returns( - 'metadata:\n' + - ' name: componentName' - ); - const result = await Component.createFromRootWorkspaceFolder(folder, [], { componentTypeName: 'componentType1' }); - expect(result.toString()).equals(`Component '${componentItem.getName()}' successfully created. Perform actions on it from Components View.`); - }); - - }); - suite('deleteConfigurationFiles', function () { let subSandbox: sinon.SinonSandbox; @@ -364,36 +252,16 @@ suite('OpenShift/Component', function () { }); - suite('describe', () => { - setup(() => { - quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); - quickPickStub.onSecondCall().resolves(componentItem); - }); + suite('describe', function() { - // Skipped due to 'null' argument value is not supported by `Component..describe' - test.skip('returns null when cancelled', async () => { - quickPickStub.onFirstCall().resolves(); - const result = await Component.describe(null); - expect(result).null; - }); - - test('calls the correct odo command', async () => { + test('calls the correct odo command', async function () { await Component.describe(componentItem1); expect(termStub).calledOnceWith(Command.describeComponent()); }); - // Skipped due to 'null' argument value is not supported by `Component..describe' - test.skip('works with no context', async () => { - await Component.describe(null); - expect(termStub).calledOnceWith(Command.describeComponent()); - }); }); - suite('component commands tree', () => { - setup(() => { - quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); - quickPickStub.onSecondCall().resolves(componentItem); - }); + suite('component commands tree', function () { test('returns correct component commands tree nodes', async () => { const treeDataProvider = ComponentsTreeDataProvider.instance; @@ -432,13 +300,9 @@ suite('OpenShift/Component', function () { }); }); - suite.skip('log', () => { - setup(() => { - quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); - quickPickStub.onSecondCall().resolves(componentItem); - }); + suite.skip('log', function () { - test('log calls the correct odo command', async () => { + test('log calls the correct odo command', async function () { await Component.log(componentItem1); expect(termStub).calledOnceWith(Command.showLog()); }); @@ -449,25 +313,21 @@ suite('OpenShift/Component', function () { }); }); - suite.skip('followLog', () => { - setup(() => { - quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); - quickPickStub.onSecondCall().resolves(componentItem); - }); + suite.skip('followLog', function() { - test('returns null when cancelled', async () => { + test('returns null when cancelled', async function () { quickPickStub.onFirstCall().resolves(); const result = await Component.followLog(null); expect(result).null; }); - test('followLog calls the correct odo command', async () => { + test('followLog calls the correct odo command', async function () { await Component.followLog(componentItem1); expect(termStub).calledOnceWith(Command.showLogAndFollow()); }); - test('works with no context', async () => { + test('works with no context', async function () { await Component.followLog(null); expect(termStub).calledOnceWith(Command.showLogAndFollow()); }); @@ -516,7 +376,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([ + sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([ new ComponentTypeAdapter( 'componentType3', undefined, @@ -568,7 +428,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); const result = await resultPromise; @@ -584,7 +444,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); const result = await resultPromise; @@ -600,7 +460,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); let caughtError; @@ -621,7 +481,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(OdoImpl.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); let caughtError; diff --git a/test/unit/openshift/nameValidator.test.ts b/test/unit/openshift/nameValidator.test.ts index 4f013e5b4..d081b2a35 100644 --- a/test/unit/openshift/nameValidator.test.ts +++ b/test/unit/openshift/nameValidator.test.ts @@ -5,7 +5,7 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; -import { OdoImpl } from '../../../src/odo'; +import { Odo } from '../../../src/odo/odoWrapper'; import * as NameValidator from '../../../src/openshift/nameValidator'; import { wait } from '../../../src/util/async'; import sinon = require('sinon'); @@ -18,7 +18,7 @@ suite('nameValidator', function () { setup(function () { sandbox = sinon.createSandbox(); - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves('cluster'); + sandbox.stub(Odo.prototype, 'getActiveCluster').resolves('cluster'); }); teardown(function () { diff --git a/test/unit/openshift/project.test.ts b/test/unit/openshift/project.test.ts index d2a765c06..f22228d63 100644 --- a/test/unit/openshift/project.test.ts +++ b/test/unit/openshift/project.test.ts @@ -8,8 +8,7 @@ import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; import { CommandText } from '../../../src/base/command'; -import { OdoImpl } from '../../../src/odo'; -import { Command } from '../../../src/odo/command'; +import { Odo } from '../../../src/odo/odoWrapper'; import { Project as OdoProject } from '../../../src/odo/project'; import { Project } from '../../../src/openshift/project'; @@ -19,6 +18,8 @@ chai.use(sinonChai); suite('OpenShift/Project', () => { let sandbox: sinon.SinonSandbox; let execStub: sinon.SinonStub; + let createProjectStub: sinon.SinonStub; + let deleteProjectStub: sinon.SinonStub; let projectItem: OdoProject; const errorMessage = 'ERROR MESSAGE'; @@ -26,9 +27,11 @@ suite('OpenShift/Project', () => { setup(() => { projectItem = { name: 'project', active: true }; sandbox = sinon.createSandbox(); - sandbox.stub(OdoImpl.prototype, 'getActiveCluster').resolves('cluster'); - sandbox.stub(OdoImpl.prototype, 'getProjects').resolves([projectItem]); - execStub = sandbox.stub(OdoImpl.prototype, 'execute').resolves({error: undefined, stdout: '', stderr: ''}); + sandbox.stub(Odo.prototype, 'getActiveCluster').resolves('cluster'); + sandbox.stub(Odo.prototype, 'getProjects').resolves([projectItem]); + execStub = sandbox.stub(Odo.prototype, 'execute').resolves({error: undefined, stdout: '', stderr: ''}); + createProjectStub = sandbox.stub(Odo.prototype, 'createProject').resolves(); + deleteProjectStub = sandbox.stub(Odo.prototype, 'deleteProject').resolves(); }); teardown(() => { @@ -46,7 +49,7 @@ suite('OpenShift/Project', () => { const result = await Project.create(); expect(result).equals(`Project '${projectItem.name}' successfully created`); - expect(execStub).calledWith(Command.createProject(projectItem.name)); + expect(createProjectStub).calledWith(projectItem.name); }); test('returns null with no project name selected', async () => { @@ -146,7 +149,7 @@ suite('OpenShift/Project', () => { const result = await Project.del(null); expect(result).equals(`Project '${projectItem.name}' successfully deleted`); - expect(`${execStub.getCall(0).args[0]}`).equals(`${Command.deleteProject(projectItem.name)}`); + expect(deleteProjectStub).to.be.calledWith(projectItem.name); }); test('returns null when cancelled', async () => { diff --git a/test/unit/openshift/testOSItem.ts b/test/unit/openshift/testOSItem.ts deleted file mode 100644 index 91c4541e9..000000000 --- a/test/unit/openshift/testOSItem.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import { Uri } from 'vscode'; -import { OpenShiftObject, ContextType } from '../../../src/odo'; -import { BuilderImage } from '../../../src/odo/builderImage'; - -export class TestItem implements OpenShiftObject { - public treeItem = null; - public active = true; - // eslint-disable-next-line no-useless-constructor - constructor( - private parent: OpenShiftObject, - private name: string, - public contextValue: ContextType, - private children = [], - public contextPath = Uri.parse('file:///c%3A/Temp'), - public path?: string, - public compType?: string) { - } - - public builderImage?: BuilderImage; - iconPath?: Uri; - description?: string; - detail?: string; - picked?: boolean; - alwaysShow?: boolean; - - getName(): string { - return this.name; - } - - getTreeItem(): null { - return this.treeItem; - } - - getChildren(): any[] { - return this.children; - } - - removeChild(item: OpenShiftObject): Promise { - return; - } - - addChild(item: OpenShiftObject): Promise { - return Promise.resolve(item); - } - - getParent(): OpenShiftObject { - return this.parent; - } - - get label(): string { - return this.name; - } - - isOdoManaged(): boolean { - return this.compType !== undefined; - } -}