diff --git a/.vscode/launch.json b/.vscode/launch.json index 608f436cdb..1be8519316 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], "stopOnEntry": false, "sourceMaps": true, - "outDir": "${workspaceRoot}/", + "outDir": "${workspaceRoot}/out", "preLaunchTask": "npm" } ] diff --git a/commands/build-image.ts b/commands/build-image.ts new file mode 100644 index 0000000000..992b3fac8c --- /dev/null +++ b/commands/build-image.ts @@ -0,0 +1,72 @@ +import vscode = require('vscode'); + + +function hasWorkspaceFolder() : boolean { + return vscode.workspace.rootPath ? true : false; +} + +function getDockerFileUris(): Thenable{ + if (!hasWorkspaceFolder()) { + return Promise.resolve(null); + } + return Promise.resolve(vscode.workspace.findFiles('**/[dD]ocker[fF]ile', null, 9999, null)); +} + +interface Item extends vscode.QuickPickItem { + path: string, + file: string +} + +function createItem(uri: vscode.Uri) : Item { + let length = vscode.workspace.rootPath.length; + let label = uri.fsPath.substr(length); + return { + label: label, + description: null, + path: '.' + label.substr(0, label.length - '/dockerfile'.length), + file: '.' + label + }; +} + +function computeItems(uris: vscode.Uri[]) : vscode.QuickPickItem[] { + let items : vscode.QuickPickItem[] = []; + for (let i = 0; i < uris.length; i++) { + items.push(createItem(uris[i])); + } + return items; +} + +export function buildImage() { + getDockerFileUris().then(function (uris: vscode.Uri[]) { + if (!uris || uris.length == 0) { + vscode.window.showInformationMessage('Couldn\'t find any dockerfile in your workspace.'); + } else { + let items: vscode.QuickPickItem[] = computeItems(uris); + vscode.window.showQuickPick(items, { placeHolder: 'Choose Dockerfile to build' }).then(function(selectedItem : Item) { + if (selectedItem) { + + let imageName = selectedItem.path.split('/').pop().toLowerCase(); + if (imageName === '.') { + imageName = vscode.workspace.rootPath.split('/').pop().toLowerCase(); + } + + let configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker'); + + let defaultRegistryPath = configOptions.get('defaultRegistryPath', ''); + if (defaultRegistryPath.length > 0) { + imageName = defaultRegistryPath + '/' + imageName; + } + + let defaultRegistry = configOptions.get('defaultRegistry', ''); + if (defaultRegistry.length > 0) { + imageName = defaultRegistry + '/' + imageName; + } + + let terminal: vscode.Terminal = vscode.window.createTerminal('Docker'); + terminal.sendText(`docker build -f ${selectedItem.file} -t ${imageName} ${selectedItem.path}`); + terminal.show(); + } + }); + } + }); +} \ No newline at end of file diff --git a/commands/docker-compose.ts b/commands/docker-compose.ts new file mode 100644 index 0000000000..94655aa323 --- /dev/null +++ b/commands/docker-compose.ts @@ -0,0 +1,63 @@ +import vscode = require('vscode'); + + +function hasWorkspaceFolder() : boolean { + return vscode.workspace.rootPath ? true : false; +} + +function getDockerComposeFileUris(): Thenable{ + if (!hasWorkspaceFolder()) { + return Promise.resolve(null); + } + return Promise.resolve(vscode.workspace.findFiles('{**/[dD]ocker-[cC]ompose.*.yml,**/[dD]ocker-[cC]ompose.yml}', null, 9999, null)); +} + +interface Item extends vscode.QuickPickItem { + path: string, + file: string +} + +function createItem(uri: vscode.Uri) : Item { + let length = vscode.workspace.rootPath.length; + let label = uri.fsPath.substr(length); + let slashIndex = label.lastIndexOf('/'); + return { + label: label, + description: null, + path: '.' + label.substr(0, slashIndex), + file: label.substr(slashIndex + 1) + }; +} + +function computeItems(uris: vscode.Uri[]) : vscode.QuickPickItem[] { + let items : vscode.QuickPickItem[] = []; + for (let i = 0; i < uris.length; i++) { + items.push(createItem(uris[i])); + } + return items; +} + +export function compose(command: string, message: string) { + getDockerComposeFileUris().then(function (uris: vscode.Uri[]) { + if (!uris || uris.length == 0) { + vscode.window.showInformationMessage('Couldn\'t find any docker-compose file in your workspace.'); + } else { + let items: vscode.QuickPickItem[] = computeItems(uris); + vscode.window.showQuickPick(items, { placeHolder: `Choose Docker Compose file ${message}` }).then(function(selectedItem : Item) { + if (selectedItem) { + let terminal: vscode.Terminal = vscode.window.createTerminal('Docker Compose'); + terminal.sendText(`cd ${selectedItem.path}; docker-compose -f ${selectedItem.file} ${command}`); + terminal.show(); + } + }); + } + }); +} + +export function composeUp() { + compose('up', 'to bring up'); +} + +export function composeDown() { + compose('down', 'to take down'); +} \ No newline at end of file diff --git a/commands/open-shell-container.ts b/commands/open-shell-container.ts new file mode 100644 index 0000000000..e88f872f1a --- /dev/null +++ b/commands/open-shell-container.ts @@ -0,0 +1,12 @@ +import vscode = require('vscode'); +import {ContainerItem, quickPickContainer} from './utils/quick-pick-container'; + +export function openShellContainer() { + quickPickContainer().then(function (selectedItem: ContainerItem) { + if (selectedItem) { + let terminal = vscode.window.createTerminal(`sh ${selectedItem.label}`); + terminal.sendText(`docker exec -it ${selectedItem.ids[0]} /bin/sh`); + terminal.show(); + } + }); +} \ No newline at end of file diff --git a/commands/push-image.ts b/commands/push-image.ts new file mode 100644 index 0000000000..262719e22f --- /dev/null +++ b/commands/push-image.ts @@ -0,0 +1,12 @@ +import vscode = require('vscode'); +import {ImageItem, quickPickImage} from './utils/quick-pick-image'; + +export function pushImage() { + quickPickImage().then(function (selectedItem: ImageItem) { + if (selectedItem) { + let terminal = vscode.window.createTerminal(selectedItem.label); + terminal.sendText(`docker push ${selectedItem.label}`); + terminal.show(); + }; + }); +} \ No newline at end of file diff --git a/commands/remove-image.ts b/commands/remove-image.ts new file mode 100644 index 0000000000..84ff412119 --- /dev/null +++ b/commands/remove-image.ts @@ -0,0 +1,15 @@ +import {docker} from './utils/docker-endpoint'; +import {ImageItem, quickPickImage} from './utils/quick-pick-image'; + + +export function removeImage() { + quickPickImage().then(function (selectedItem: ImageItem) { + if (selectedItem) { + let image = docker.getImage(selectedItem.id); + image.remove({ force: true }, function (err, data) { + // console.log("Removed - error: " + err); + // console.log("Removed - data: " + data); + }); + } + }); +} \ No newline at end of file diff --git a/commands/showlogs-container.ts b/commands/showlogs-container.ts new file mode 100644 index 0000000000..3a4022ccdd --- /dev/null +++ b/commands/showlogs-container.ts @@ -0,0 +1,13 @@ +import vscode = require('vscode'); +import {ContainerItem, quickPickContainer} from './utils/quick-pick-container'; + + +export function showLogsContainer() { + quickPickContainer().then(function (selectedItem: ContainerItem) { + if (selectedItem) { + let terminal = vscode.window.createTerminal(selectedItem.label); + terminal.sendText(`docker logs ${selectedItem.ids[0]}`); + terminal.show(); + } + }); +} \ No newline at end of file diff --git a/commands/start-container.ts b/commands/start-container.ts new file mode 100644 index 0000000000..5c7c6772d8 --- /dev/null +++ b/commands/start-container.ts @@ -0,0 +1,22 @@ +import vscode = require('vscode'); +import {ImageItem, quickPickImage} from './utils/quick-pick-image'; + + +function doStartContainer(interactive: boolean) { + quickPickImage().then(function (selectedItem: ImageItem) { + if (selectedItem) { + let option = interactive ? '-it' : ''; + let terminal = vscode.window.createTerminal(selectedItem.label); + terminal.sendText(`docker run ${option} --rm ${selectedItem.label}`); + terminal.show(); + } + }); +} + +export function startContainer() { + doStartContainer(false); +} + +export function startContainerInteractive() { + doStartContainer(true); +} \ No newline at end of file diff --git a/commands/stop-container.ts b/commands/stop-container.ts new file mode 100644 index 0000000000..2955e983a5 --- /dev/null +++ b/commands/stop-container.ts @@ -0,0 +1,17 @@ +import {docker} from './utils/docker-endpoint'; +import {ContainerItem, quickPickContainer} from './utils/quick-pick-container'; + + +export function stopContainer() { + quickPickContainer(true).then(function (selectedItem: ContainerItem) { + if (selectedItem) { + for (let i = 0; i < selectedItem.ids.length; i++) { + let container = docker.getContainer(selectedItem.ids[i]); + container.stop(function (err, data) { + // console.log("Stopped - error: " + err); + // console.log("Stopped - data: " + data); + }); + } + } + }); +} \ No newline at end of file diff --git a/commands/utils/docker-endpoint.ts b/commands/utils/docker-endpoint.ts new file mode 100644 index 0000000000..b3a7cc894f --- /dev/null +++ b/commands/utils/docker-endpoint.ts @@ -0,0 +1,47 @@ +import * as Docker from 'dockerode'; + + +class DockerClient { + + private endPoint:Docker; + + constructor() { + if (process.platform === 'win32') { + this.endPoint = new Docker({ socketPath: "//./pipe/docker_engine" }); + } else { + this.endPoint = new Docker({ socketPath: '/var/run/docker.sock' }); + } + } + + public getContainerDescriptors(): Thenable{ + return new Promise((resolve, reject) => { + this.endPoint.listContainers((err, containers) => { + if (err) { + return reject(err); + } + return resolve(containers); + }); + }); + }; + + public getImageDescriptors(): Thenable{ + return new Promise((resolve, reject) => { + this.endPoint.listImages((err, images) => { + if (err) { + return reject(err); + } + return resolve(images); + }); + }); + }; + + public getContainer(id: string): Docker.Container { + return this.endPoint.getContainer(id); + } + + public getImage(id:string): Docker.Image { + return this.endPoint.getImage(id); + } +} + +export const docker = new DockerClient(); \ No newline at end of file diff --git a/commands/utils/quick-pick-container.ts b/commands/utils/quick-pick-container.ts new file mode 100644 index 0000000000..31b87a5560 --- /dev/null +++ b/commands/utils/quick-pick-container.ts @@ -0,0 +1,50 @@ +import * as Docker from 'dockerode'; +import {docker} from './docker-endpoint'; +import vscode = require('vscode'); + + +export interface ContainerItem extends vscode.QuickPickItem { + ids: string[] +} + +function createItem(container: Docker.ContainerDesc) : ContainerItem { + return { + label: container.Image, + description: container.Status, + ids: [ container.Id ] + }; +} + +function computeItems(containers: Docker.ContainerDesc[], includeAll: boolean) : ContainerItem[] { + + let allIds: string[] = []; + + let items : ContainerItem[] = []; + for (let i = 0; i < containers.length; i++) { + let item = createItem(containers[i]); + allIds.push(item.ids[0]); + items.push(item); + } + + if (includeAll && allIds.length > 0) { + items.unshift( { + label: 'All Containers', + description: 'Stops all running containers', + ids: allIds + }); + } + + return items; +} + +export function quickPickContainer(includeAll: boolean = false) : Thenable{ + return docker.getContainerDescriptors().then(containers => { + if (!containers || containers.length == 0) { + vscode.window.showInformationMessage('There are no running docker containers.'); + return Promise.resolve(null); + } else { + let items: ContainerItem[] = computeItems(containers, includeAll); + return vscode.window.showQuickPick(items, { placeHolder: 'Choose Container' }); + } + }); +} \ No newline at end of file diff --git a/commands/utils/quick-pick-image.ts b/commands/utils/quick-pick-image.ts new file mode 100644 index 0000000000..be0358ec97 --- /dev/null +++ b/commands/utils/quick-pick-image.ts @@ -0,0 +1,37 @@ +import * as Docker from 'dockerode'; +import {docker} from './docker-endpoint'; +import vscode = require('vscode'); + + + +export interface ImageItem extends vscode.QuickPickItem { + id: string, +} + +function createItem(image: Docker.ImageDesc) : ImageItem { + return { + label: image.RepoTags[0] || '', + description: null, + id: image.Id + }; +} + +function computeItems(images: Docker.ImageDesc[]) : ImageItem[] { + let items : ImageItem[] = []; + for (let i = 0; i < images.length; i++) { + items.push(createItem(images[i])); + } + return items; +} + +export function quickPickImage() : Thenable { + return docker.getImageDescriptors().then(images => { + if (!images || images.length == 0) { + vscode.window.showInformationMessage('There are no docker images yet. Try Build first.'); + return Promise.resolve(null); + } else { + let items: ImageItem[] = computeItems(images); + return vscode.window.showQuickPick(items, { placeHolder: 'Choose image' }); + } + }); +} \ No newline at end of file diff --git a/dockerExtension.ts b/dockerExtension.ts index 1e976c6ff7..9c4d5a3301 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -10,6 +10,14 @@ import {DOCKER_COMPOSE_KEY_INFO} from './dockerCompose/dockerComposeKeyInfo'; import {DockerComposeParser} from './dockerCompose/dockerComposeParser'; import {DockerfileParser} from './dockerfile/dockerfileParser'; import vscode = require('vscode'); +import {buildImage} from './commands/build-image'; +import {removeImage} from './commands/remove-image'; +import {pushImage} from './commands/push-image'; +import {startContainer, startContainerInteractive} from './commands/start-container'; +import {stopContainer} from './commands/stop-container'; +import {showLogsContainer} from './commands/showlogs-container'; +import {openShellContainer} from './commands/open-shell-container'; +import {composeUp, composeDown} from './commands/docker-compose'; export function activate(ctx: vscode.ExtensionContext): void { const DOCKERFILE_MODE_ID: vscode.DocumentFilter = { language: 'dockerfile', scheme: 'file' }; @@ -20,5 +28,16 @@ export function activate(ctx: vscode.ExtensionContext): void { const YAML_MODE_ID: vscode.DocumentFilter = { language: 'yaml', scheme: 'file', pattern: '**/docker-compose*.yml' }; var yamlHoverProvider = new DockerHoverProvider(new DockerComposeParser(), DOCKER_COMPOSE_KEY_INFO); ctx.subscriptions.push(vscode.languages.registerHoverProvider(YAML_MODE_ID, yamlHoverProvider)); - ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(YAML_MODE_ID, new DockerComposeCompletionItemProvider(), '.')) + ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(YAML_MODE_ID, new DockerComposeCompletionItemProvider(), '.')); + + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.image.build', buildImage)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.image.remove', removeImage)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.image.push', pushImage)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.container.start', startContainer)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.container.start.interactive', startContainerInteractive)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.container.stop', stopContainer)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.container.show-logs', showLogsContainer)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.container.open-shell', openShellContainer)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.compose.up', composeUp)); + ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.compose.down', composeDown)); } \ No newline at end of file diff --git a/package.json b/package.json index f5a447ab86..61b6a8d9a7 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "vscode-docker", - "version": "0.0.6", + "name": "docker", + "version": "0.0.7", "publisher": "PeterJausovec", - "displayName": "Dockerfile and Docker Compose File (yml) Support", - "description": "Adds syntax highlighting, snippets and description on hover for Dockerfiles and Docker compose files.", + "displayName": "Docker Support", + "description": "Adds syntax highlighting, snippets, commands and description on hover for Dockerfiles and Docker compose files.", "license": "SEE LICENSE IN LICENSE.md", "icon": "images/docker_icon.svg", "galleryBanner": { @@ -11,20 +11,19 @@ "theme": "dark" }, "homepage": "https://github.com/Microsoft/vscode-docker/blob/master/README.md", - "bugs": { - "url": "https://github.com/Microsoft/vscode-docker/issues" - }, - "categories": [ - "Languages" - ], - "private": true, - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-docker.git" - }, "activationEvents": [ "onLanguage:dockerfile", - "onLanguage:yaml" + "onLanguage:yaml", + "onCommand:vscode-docker.image.build", + "onCommand:vscode-docker.image.remove", + "onCommand:vscode-docker.image.push", + "onCommand:vscode-docker.container.start", + "onCommand:vscode-docker.container.start.interactive", + "onCommand:vscode-docker.container.stop", + "onCommand:vscode-docker.container.show-logs", + "onCommand:vscode-docker.container.open-shell", + "onCommand:vscode-docker.compose.up", + "onCommand:vscode-docker.compose.down" ], "main": "./out/dockerExtension", "contributes": { @@ -45,13 +44,81 @@ "Dockerfile*.*" ] } + ], + "configuration": { + "type": "object", + "title": "Docker configuration options", + "properties": { + "docker.defaultRegistry": { + "type": "string", + "default": "", + "description": "Default registry to push to, empty string will push to Dockerhub." + }, + "docker.defaultRegistryPath": { + "type": "string", + "default": "", + "description": "Path within registry to push to." + } + } + }, + "commands": [ + { + "command": "vscode-docker.image.build", + "title": "Docker: Build Image", + "description": "Build a Docker image from a dockerfile" + }, + { + "command": "vscode-docker.image.remove", + "title": "Docker: Remove Image", + "description": "Remove a Docker image" + }, + { + "command": "vscode-docker.container.start", + "title": "Docker: Run", + "description": "Starts a container from an image" + }, + { + "command": "vscode-docker.container.start.interactive", + "title": "Docker: Run Interactive", + "description": "Starts a container from an image and runs it interactively" + }, + { + "command": "vscode-docker.container.stop", + "title": "Docker: Stop", + "description": "Stop a running container" + }, + { + "command": "vscode-docker.container.show-logs", + "title": "Docker: Show Logs", + "description": "Show the logs of a running container" + }, + { + "command": "vscode-docker.container.open-shell", + "title": "Docker: Attach Shell", + "description": "Open a terminal with an interactive shell for a running container" + }, + { + "command": "vscode-docker.compose.up", + "title": "Docker: Compose Up", + "description": "Starts a composition of containers" + }, + { + "command": "vscode-docker.compose.down", + "title": "Docker: Compose Down", + "description": "Stops a composition of containers" + }, + { + "command": "vscode-docker.image.push", + "title": "Docker: Push", + "description": "Push an image to a registry" + } ] }, "engines": { - "vscode": "^0.10.8" + "vscode": "^1.5.1" }, "scripts": { - "vscode:prepublish": "tsc", + "vscode:prepublish": "./node_modules/typescript/bin/tsc", "postinstall": "node ./node_modules/vscode/bin/install", "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" }, @@ -60,6 +127,10 @@ "vscode.yaml" ], "devDependencies": { - "vscode": "0.11.x" + "vscode": "0.11.x", + "typescript": "1.8.x" + }, + "dependencies": { + "dockerode": "^2.3.0" } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 99b0719a8c..330e1173d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "out", "noLib": true, "sourceMap": true, - "target": "es5" + "target": "es6" }, "exclude": [ "node_modules" diff --git a/typings/dockerode.d.ts b/typings/dockerode.d.ts new file mode 100644 index 0000000000..df58e2b4d8 --- /dev/null +++ b/typings/dockerode.d.ts @@ -0,0 +1,82 @@ +declare module Docker { + interface Modem { + demuxStream(stream: NodeJS.ReadWriteStream, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream): void; + } + + interface DockerOptions { + socketPath: string; + } + + interface HostConfig { + Binds: string[]; + } + + interface CreateContainerOptions { + Image: string; + Volumes: { [path: string]: any; }; + ExposedPorts?: any; + HostConfig: HostConfig; + } + + interface StartOptions { + hijack?: boolean; + stdin?: boolean; + } + + interface ExecInspectData { + ExitCode: number; + } + + class Exec { + start(options: StartOptions, cb: (err: Error, stream: NodeJS.ReadWriteStream)=>void): void; + inspect(cb: (err: Error, data: ExecInspectData)=>void): void; + } + + interface ExecOptions { + AttachStdin?: boolean; + AttachStdout?: boolean; + AttachStderr?: boolean; + Tty?: boolean; + Cmd?: string[]; + } + + class Container { + start(options: any, cb: (err: Error, data: any)=>void): void; + start(cb: (err: Error, data: any)=>void): void; + stop(cb: (err: Error, data: any)=>void): void; + exec(options: ExecOptions, cb: (err: Error, exec: Exec)=>void): void; + } + + class Image { + remove(options: any, cb: (err: Error, exec: Exec)=>void): void; + } + + interface ImageDesc { + Id: string; + ParentId: string; + RepoTags: string[] + } + + interface ContainerDesc { + Id: string; + Image: string; + Status: string; + } +} + + +declare class Docker { + modem: Docker.Modem; + constructor(options: Docker.DockerOptions); + + listImages(cb: (err:Error , images: Docker.ImageDesc[])=>void): void; + getImage(id:string): Docker.Image; + + createContainer(options: Docker.CreateContainerOptions, cb: (err: Error, container: Docker.Container)=>void): void; + listContainers(cb: (err:Error , containers: Docker.ContainerDesc[])=>void): void; + getContainer(id: string): Docker.Container; +} + +declare module "dockerode" { + export = Docker; +} \ No newline at end of file