Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy container image to cluster #3798

Merged
merged 1 commit into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added images/walkthrough/deploy-a-container-image.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 50 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@
},
{
"command": "openshift.helm.openView",
"title": "Open Helm View",
"title": "Browse and Install Helm Charts",
"category": "OpenShift"
},
{
Expand Down Expand Up @@ -755,11 +755,26 @@
"title": "Create Operator-Backed Service",
"category": "OpenShift"
},
{
"command": "openshift.deployment.create.fromImageUrl",
"title": "Create Deployment from Container Image URL",
"category": "OpenShift"
},
{
"command": "openshift.resource.load",
"title": "Load",
"category": "OpenShift"
},
{
"command": "openshift.resource.delete",
"title": "Delete",
"category": "OpenShift"
},
{
"command": "openshift.resource.watchLogs",
"title": "Watch Logs",
"category": "OpenShift"
},
{
"command": "openshift.resource.unInstall",
"title": "Uninstall",
Expand Down Expand Up @@ -944,7 +959,7 @@
},
{
"id": "view/item/context/createService",
"label": "Create Service"
"label": "Create..."
}
],
"viewsContainers": {
Expand Down Expand Up @@ -1242,6 +1257,14 @@
"command": "openshift.resource.load",
"when": "false"
},
{
"command": "openshift.resource.delete",
"when": "false"
},
{
"command": "openshift.resource.watchLogs",
"when": "false"
},
{
"command": "openshift.resource.unInstall",
"when": "false"
Expand Down Expand Up @@ -1494,6 +1517,11 @@
"command": "openshift.service.create",
"when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && showCreateService",
"group": "c2"
},
{
"command": "openshift.deployment.create.fromImageUrl",
"when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i",
"group": "c2"
}
],
"view/item/context": [
Expand Down Expand Up @@ -1752,6 +1780,14 @@
"command": "openshift.resource.load",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sObject || viewItem == openshift.k8sObject.route"
},
{
"command": "openshift.resource.delete",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sObject || viewItem == openshift.k8sObject.route"
},
{
"command": "openshift.resource.watchLogs",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sObject || viewItem == openshift.k8sObject.route"
},
{
"command": "openshift.resource.unInstall",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sObject.helm"
Expand Down Expand Up @@ -1903,6 +1939,18 @@
"onCommand:openshift.helm.openView"
]
},
{
"id": "deployContainerImage",
"title": "Deploy Container Image from URL",
"description": "In the Application Explorer sidebar panel, expand the tree item corresponding to the OpenShift/Kubernetes cluster, then right click on the project, and select \"Create...\" > \"Create Deployment from Container Image URL\". You will be asked to enter the URL to the container image and enter a name for the deployment. Once you've submitted this information, the Deployment will be created, and the logs for the first container created as a part of the Deployment will be opened in the OpenShift Terminal.",
"media": {
"image": "images/walkthrough/deploy-a-container-image.gif",
"altText": "Creating a Deployment of the Docker Hub MongoDB container image called my-mongo-db using the steps from the description"
},
"completionEvents": [
"onCommand:openshift.deployment.create.fromImageUrl"
]
},
{
"id": "startDevComponent",
"title": "Start a component in development mode",
Expand Down
206 changes: 206 additions & 0 deletions src/deployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*-----------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*-----------------------------------------------------------------------------------------------*/

import * as path from 'path';
import validator from 'validator';
import { Disposable, QuickInputButtons, ThemeIcon, TreeItem, window } from 'vscode';
import { OpenShiftExplorer } from './explorer';
import { Oc } from './oc/ocWrapper';
import { validateRFC1123DNSLabel } from './openshift/nameValidator';
import { quickBtn } from './util/inputValue';
import { vsCommand } from './vscommand';

export class Deployment {

@vsCommand('openshift.deployment.create.fromImageUrl')
static async createFromImageUrl(context: TreeItem): Promise<void> {

enum State {
SelectImage, SelectName
}

let state: State = State.SelectImage;
let imageUrl: string;

while (state !== undefined) {

switch (state) {

case State.SelectImage: {

imageUrl = await Deployment.getImageUrl(false, imageUrl);

if (imageUrl === null || imageUrl === undefined) {
return;
}
state = State.SelectName;
break;
}

case State.SelectName: {
let cleanedUrl = imageUrl.startsWith('https://') ? imageUrl : `https://${imageUrl}`;
if (cleanedUrl.lastIndexOf('/') > 0
&& cleanedUrl.substring(cleanedUrl.lastIndexOf('/')).indexOf(':') >=0) {
// it has a version tag, which we need to clean for the
cleanedUrl = cleanedUrl.substring(0, cleanedUrl.lastIndexOf(':'));
}
const imageUrlAsUrl = new URL(cleanedUrl);
const suggestedName = `my-${path.basename(imageUrlAsUrl.pathname)}`;

const deploymentName = await Deployment.getDeploymentName(suggestedName, true);

if (deploymentName === null) {
return;
} else if (deploymentName === undefined) {
state = State.SelectImage;
break;
}

await Oc.Instance.createDeploymentFromImage(deploymentName, imageUrl);
void window.showInformationMessage(`Created deployment '${deploymentName}' from image '${imageUrl}'`);
OpenShiftExplorer.getInstance().refresh(context);

void OpenShiftExplorer.watchLogs({
kind: 'Deployment',
metadata: {
name: deploymentName
}
});

return;
}
default:
}

}

}

/**
* Prompt the user for the URL of a container image.
*
* @returns the selected container image URL, or undefined if "back" was requested, and null if "cancel" was requested
*/
private static async getImageUrl(allowBack: boolean, initialValue?: string): Promise<string> {
return new Promise<string | null | undefined>(resolve => {
const disposables: Disposable[] = [];

const cancelBtn = new quickBtn(new ThemeIcon('close'), 'Cancel');
const okBtn = new quickBtn(new ThemeIcon('check'), 'Ok');

const inputBox = window.createInputBox();
datho7561 marked this conversation as resolved.
Show resolved Hide resolved
inputBox.placeholder = 'docker.io/library/mongo';
inputBox.title = 'Image URL';
inputBox.value = initialValue;
if (allowBack) {
inputBox.buttons = [QuickInputButtons.Back, okBtn, cancelBtn];
} else {
inputBox.buttons = [okBtn, cancelBtn];
}
inputBox.ignoreFocusOut = true;

disposables.push(inputBox.onDidHide(() => resolve(null)));

disposables.push(inputBox.onDidChangeValue((e) => {
if (validator.isURL(inputBox.value)) {
inputBox.validationMessage = undefined;
} else {
inputBox.validationMessage = 'Please enter a valid URL';
}
}));

disposables.push(inputBox.onDidAccept((e) => {
if (inputBox.validationMessage === undefined && inputBox.value !== undefined) {
resolve(inputBox.value);
inputBox.hide();
disposables.forEach(disposable => {disposable.dispose()});
}
}));

disposables.push(inputBox.onDidTriggerButton((button) => {
if (button === QuickInputButtons.Back) {
inputBox.hide();
resolve(undefined);
} else if (button === cancelBtn) {
inputBox.hide();
resolve(null);
} else if (button === okBtn) {
if (inputBox.validationMessage === undefined && inputBox.value !== undefined) {
inputBox.hide();
resolve(inputBox.value);
disposables.forEach(disposable => {disposable.dispose()});
}
}
}));

inputBox.show();
});
}

/**
* Prompt the user for the name of the deployment.
*
* @returns the selected deployment name, or undefined if "back" was requested, and null if "cancel" was requested
*/
private static async getDeploymentName(suggestedName: string, allowBack: boolean): Promise<string> {
return new Promise<string | null | undefined>(resolve => {
const disposables: Disposable[] = [];

const cancelBtn = new quickBtn(new ThemeIcon('close'), 'Cancel');
const okBtn = new quickBtn(new ThemeIcon('check'), 'Ok');

const inputBox = window.createInputBox();
datho7561 marked this conversation as resolved.
Show resolved Hide resolved
inputBox.placeholder = suggestedName;
inputBox.value = suggestedName;
inputBox.title = 'Deployment Name';
if (allowBack) {
inputBox.buttons = [QuickInputButtons.Back, okBtn, cancelBtn];
} else {
inputBox.buttons = [okBtn, cancelBtn];
}
inputBox.ignoreFocusOut = true;

disposables.push(inputBox.onDidHide(() => resolve(null)));

disposables.push(inputBox.onDidChangeValue((e) => {
if (inputBox.value === undefined) {
inputBox.validationMessage = undefined;
} else {
inputBox.validationMessage = validateRFC1123DNSLabel('Must be a valid Kubernetes name', inputBox.value);
if (inputBox.validationMessage.length === 0) {
inputBox.validationMessage = undefined;
}
}
}));

disposables.push(inputBox.onDidAccept((e) => {
if (inputBox.validationMessage === undefined && inputBox.value !== undefined) {
resolve(inputBox.value);
inputBox.hide();
disposables.forEach(disposable => {disposable.dispose()});
}
}));

disposables.push(inputBox.onDidTriggerButton((button) => {
if (button === QuickInputButtons.Back) {
inputBox.hide();
resolve(undefined);
} else if (button === cancelBtn) {
inputBox.hide();
resolve(null);
} else if (button === okBtn) {
if (inputBox.validationMessage === undefined && inputBox.value !== undefined) {
resolve(inputBox.value);
inputBox.hide();
disposables.forEach(disposable => {disposable.dispose()});
}
}
}));

inputBox.show();
});
}

}
31 changes: 31 additions & 0 deletions src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
version,
window
} from 'vscode';
import { CommandText } from './base/command';
import * as Helm from './helm/helm';
import { HelmRepo } from './helm/helmChartType';
import { Oc } from './oc/ocWrapper';
Expand All @@ -33,6 +34,7 @@ import { Progress } from './util/progress';
import { FileContentChangeNotifier, WatchUtil } from './util/watch';
import { vsCommand } from './vscommand';
import { CustomResourceDefinitionStub } from './webview/common/createServiceTypes';
import { OpenShiftTerminalManager } from './webview/openshift-terminal/openShiftTerminal';

type ExplorerItem = KubernetesObject | Helm.HelmRelease | Context | TreeItem | OpenShiftObject | HelmRepo;

Expand Down Expand Up @@ -352,6 +354,35 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
void commands.executeCommand('extension.vsKubernetesLoad', { namespace: component.metadata.namespace, kindName: `${component.kind}/${component.metadata.name}` });
}

@vsCommand('openshift.resource.delete')
public static async deleteResource(component: KubernetesObject) {
await Oc.Instance.deleteKubernetesObject(component.kind, component.metadata.name);
void window.showInformationMessage(`Deleted the '${component.kind}' named '${component.metadata.name}'`);
OpenShiftExplorer.instance.refresh();
}

@vsCommand('openshift.resource.watchLogs')
public static async watchLogs(component: KubernetesObject) {
// wait until logs are available before starting to stream them
await Progress.execFunctionWithProgress(`Opening ${component.kind}/${component.metadata.name} logs...`, (_) => {
return new Promise<void>(resolve => {

let intervalId: NodeJS.Timer = undefined;

function checkForPod() {
void Oc.Instance.getLogs('Deployment', component.metadata.name).then((logs) => {
clearInterval(intervalId);
resolve();
}).catch(_e => {});
}

intervalId = setInterval(checkForPod, 200);
});
});

void OpenShiftTerminalManager.getInstance().createTerminal(new CommandText('oc', `logs -f ${component.kind}/${component.metadata.name}`), `Watching '${component.metadata.name}' logs`);
}

@vsCommand('openshift.resource.unInstall')
public static async unInstallHelmChart(release: Helm.HelmRelease) {
return Progress.execFunctionWithProgress(`Uninstalling ${release.name}`, async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export async function activate(extensionContext: ExtensionContext): Promise<unkn
'./webview/devfile-registry/registryViewLoader',
'./webview/helm-chart/helmChartLoader',
'./webview/helm-manage-repository/manageRepositoryLoader',
'./feedback'
'./feedback',
'./deployment'
)),
commands.registerCommand('clusters.openshift.useProject', (context) =>
commands.executeCommand('extension.vsKubernetesUseNamespace', context),
Expand Down
Loading
Loading