Skip to content

Commit f007c49

Browse files
Merge pull request #450 from jpogran/GH-375-docker-docker-docker
(GH-375) Implement Docker connection handler
2 parents 7301c3e + e77bc46 commit f007c49

File tree

8 files changed

+267
-47
lines changed

8 files changed

+267
-47
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
- [PDK Validate](#pdk-validate)
3333
- [PDK Test Unit](#pdk-test-unit)
3434
- [Debugging Puppet manifests](#debugging-puppet-manifests)
35+
- [Docker Language Server Support](#docker-language-server-support)
3536
- [Installing the Extension](#installing-the-extension)
3637
- [Configuration](#configuration)
3738
- [Experience a Problem?](#experience-a-problem)
@@ -69,6 +70,7 @@ This extension provides full Puppet Language support for [Visual Studio Code](ht
6970
- Node graph preview
7071
- Puppet Development Kit integration
7172
- (Experimental) Local debugging of Puppet manifests
73+
- (Experimental) Docker Language Server support
7274

7375
**It is currently in technical preview, so that we can gather bug reports and find out what new features to add.**
7476

@@ -275,6 +277,23 @@ The [VSCode Debugging - Launch Configurations](https://code.visualstudio.com/doc
275277

276278
- `args` - Additional arguements to pass to `puppet apply`, for example `['--debug']` will output debug information
277279

280+
### Docker Language Server Support
281+
282+
**Note - This is an experimental feature**
283+
284+
The Puppet VSCode extension bundles the Puppet Language Server inside the extension, and loads the language server on demaned and communicates it with either STDIO or TCP. Another option is to communicate via TCP pointed towards a docker container running the Puppet Language Server. The Lingua-Pupuli organization maintains a Puppet Language Server docker container here: https://github.com/lingua-pupuli/images. Using this docker image, we can run the Puppet Language Server without having Puppet Agent or the Puppet Development Kit installed locally, all that is needed is a working docker installation.
285+
286+
Enable using docker by adding the following configuration:
287+
288+
```json
289+
{
290+
"puppet.editorService.protocol":"docker",
291+
"puppet.editorService.docker.imageName":"linguapupuli/puppet-language-server:latest"
292+
}
293+
```
294+
295+
This will cause the Puppet Extension to instruct docker to start the linguapupuli/puppet-language-server container, then wait for it to start. After starting, the Puppet Extension will use the docker container to perform the same functionality as if it was running locally.
296+
278297
## Installing the Extension
279298

280299
You can install the official release of the Puppet extension by following the steps in the [Visual Studio Code documentation](https://code.visualstudio.com/docs/editor/extension-gallery). In the Extensions pane, search for "puppet-vscode" extension and install it there. You will get notified automatically about any future extension updates!

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,11 @@
284284
"default": "",
285285
"description": "The absolute filepath where the Puppet Editor Service will output the debugging log. By default no logfile is generated"
286286
},
287+
"puppet.editorService.docker.imageName": {
288+
"type": "string",
289+
"default": "linguapupuli/puppet-language-server:latest",
290+
"description": "The name of the image with tag that contains the Puppet Language server. For example: linguapupuli/puppet-language-server:latest"
291+
},
287292
"puppet.editorService.featureFlags": {
288293
"type": "array",
289294
"default": [],
@@ -307,7 +312,8 @@
307312
"description": "The protocol used to communicate with the Puppet Editor Service. By default the local STDIO protocol is used",
308313
"enum": [
309314
"stdio",
310-
"tcp"
315+
"tcp",
316+
"docker"
311317
]
312318
},
313319
"puppet.editorService.puppet.confdir": {

src/extension.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import { PuppetResourceFeature } from './feature/PuppetResourceFeature';
1616
import { ProtocolType, ConnectionType, IConnectionConfiguration } from './interfaces';
1717
import { ILogger } from './logging';
1818
import { OutputChannelLogger } from './logging/outputchannel';
19-
import { PuppetCommandStrings } from './messages';
2019
import { PuppetStatusBar } from './PuppetStatusBar';
2120
import { ISettings, legacySettings, settingsFromWorkspace } from './settings';
2221
import { Reporter, reporter } from './telemetry/telemetry';
22+
import { DockerConnectionHandler } from './handlers/docker';
2323

2424
export const puppetLangID = 'puppet'; // don't change this
2525
export const puppetFileLangID = 'puppetfile'; // don't change this
@@ -51,21 +51,24 @@ export function activate(context: vscode.ExtensionContext) {
5151
return;
5252
}
5353

54-
if(checkInstallDirectory(configSettings, logger) === false){
54+
if(checkInstallDirectory(settings, configSettings, logger) === false){
5555
// If this returns false, then we needed a local directory
5656
// but did not find it, so we should abort here
5757
// If we return true, we can continue
5858
// This can be revisited to enable disabling language server portion
5959
return;
6060
}
6161

62-
switch (configSettings.protocol) {
62+
switch (settings.editorService.protocol) {
6363
case ProtocolType.STDIO:
6464
connectionHandler = new StdioConnectionHandler(extContext, settings, statusBar, logger, configSettings);
6565
break;
6666
case ProtocolType.TCP:
6767
connectionHandler = new TcpConnectionHandler(extContext, settings, statusBar, logger, configSettings);
6868
break;
69+
case ProtocolType.DOCKER:
70+
connectionHandler = new DockerConnectionHandler(extContext, settings, statusBar, logger, configSettings);
71+
break;
6972
}
7073

7174
extensionFeatures = [
@@ -105,8 +108,11 @@ function checkForLegacySettings() {
105108
}
106109
}
107110

108-
function checkInstallDirectory(configSettings: IConnectionConfiguration, logger: ILogger) : boolean {
109-
if(configSettings.protocol === ProtocolType.TCP){
111+
function checkInstallDirectory(settings: ISettings, configSettings: IConnectionConfiguration, logger: ILogger) : boolean {
112+
if(settings.editorService.protocol === ProtocolType.DOCKER){
113+
return true;
114+
}
115+
if(settings.editorService.protocol === ProtocolType.TCP){
110116
if(configSettings.type === ConnectionType.Remote){
111117
// Return if we are connecting to a remote TCP LangServer
112118
return true;

src/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export abstract class ConnectionHandler {
102102
this.setConnectionStatus('Stopped languageserver', ConnectionStatus.Stopped, '');
103103
}
104104

105-
private setConnectionStatus(message: string, status: ConnectionStatus, toolTip?: string) {
105+
public setConnectionStatus(message: string, status: ConnectionStatus, toolTip?: string) {
106106
this._status = status;
107107
this.statusBar.setConnectionStatus(message, status, toolTip);
108108
}

src/handlers/docker.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import * as vscode from 'vscode';
2+
import * as net from 'net';
3+
import * as cp from 'child_process';
4+
import { ServerOptions, Executable, StreamInfo } from 'vscode-languageclient';
5+
6+
import { ConnectionHandler } from '../handler';
7+
import { ConnectionStatus, ConnectionType, IConnectionConfiguration } from '../interfaces';
8+
import { ISettings } from '../settings';
9+
import { PuppetStatusBar } from '../PuppetStatusBar';
10+
import { OutputChannelLogger } from '../logging/outputchannel';
11+
import { CommandEnvironmentHelper } from '../helpers/commandHelper';
12+
13+
export class DockerConnectionHandler extends ConnectionHandler {
14+
private name: string;
15+
16+
constructor(
17+
context: vscode.ExtensionContext,
18+
settings: ISettings,
19+
statusBar: PuppetStatusBar,
20+
logger: OutputChannelLogger,
21+
config: IConnectionConfiguration,
22+
) {
23+
super(context, settings, statusBar, logger, config);
24+
this.logger.debug(`Configuring ${ConnectionType[this.connectionType]}::${this.protocolType} connection handler`);
25+
26+
/*
27+
The docker container will be assigned a random port on creation, so we don't
28+
know it unitl we ask via a docker command. Using the unique name created in
29+
createUniqueDockerName() we can get the port that the Puppet Language
30+
Server is running on in getDockerPort().
31+
*/
32+
this.name = this.createUniqueDockerName();
33+
34+
let exe: Executable = this.getDockerExecutable(this.name, this.settings.editorService.docker.imageName);
35+
this.logger.debug('Editor Services will invoke with: ' + exe.command + ' ' + exe.args.join(' '));
36+
37+
/*
38+
We start the docker container and then listen on stdout for the line that
39+
indicates the Puppet Language Server is running and ready to accept
40+
connections. This takes some time, so we can't call start() right away.
41+
We then call getDockerPort to get the port to connect to.
42+
*/
43+
var proc = cp.spawn(exe.command, exe.args);
44+
var isRunning: boolean = false;
45+
proc.stdout.on('data', data => {
46+
if (/LANGUAGE SERVER RUNNING/.test(data.toString())) {
47+
settings.editorService.tcp.port = this.getDockerPort(this.name);
48+
isRunning = true;
49+
this.start();
50+
}
51+
if (!isRunning) {
52+
this.logger.debug('Editor Service STDOUT: ' + data.toString());
53+
}
54+
});
55+
proc.stderr.on('data', data => {
56+
if (!isRunning) {
57+
this.logger.debug('Editor Service STDERR: ' + data.toString());
58+
}
59+
});
60+
proc.on('close', exitCode => {
61+
this.logger.debug('Editor Service terminated with exit code: ' + exitCode);
62+
if (!isRunning) {
63+
this.setConnectionStatus('Failure', ConnectionStatus.Failed, 'Could not start the docker container');
64+
}
65+
});
66+
}
67+
68+
// This is always a remote connection
69+
get connectionType(): ConnectionType {
70+
return ConnectionType.Remote;
71+
}
72+
73+
createServerOptions(): ServerOptions {
74+
let serverOptions = () => {
75+
let socket = net.connect({
76+
port: this.settings.editorService.tcp.port,
77+
host: this.settings.editorService.tcp.address,
78+
});
79+
80+
let result: StreamInfo = {
81+
writer: socket,
82+
reader: socket,
83+
};
84+
return Promise.resolve(result);
85+
};
86+
return serverOptions;
87+
}
88+
89+
/*
90+
Options defined in getDockerArguments() should ensure docker cleans up
91+
the container on exit, but we do this to ensure the container goes away
92+
*/
93+
cleanup(): void {
94+
this.stopLanguageServerDockerProcess(this.name);
95+
}
96+
97+
/*
98+
Unlike stdio or tcp, we don't much care about the shell env variables when
99+
starting docker containers. We only need docker on the PATH in order for
100+
this to work, so we copy what's already there and leave most of it be.
101+
*/
102+
private getDockerExecutable(containerName: string, imageName: string): Executable {
103+
let exe: Executable = {
104+
command: this.getDockerCommand(process.platform),
105+
args: this.getDockerArguments(containerName, imageName),
106+
options: {},
107+
};
108+
109+
exe.options.env = CommandEnvironmentHelper.shallowCloneObject(process.env);
110+
exe.options.stdio = 'pipe';
111+
112+
switch (process.platform) {
113+
case 'win32':
114+
break;
115+
default:
116+
exe.options.shell = true;
117+
break;
118+
}
119+
120+
CommandEnvironmentHelper.cleanEnvironmentPath(exe);
121+
122+
// undefined or null values still appear in the child spawn environment variables
123+
// In this case these elements should be removed from the Object
124+
CommandEnvironmentHelper.removeEmptyElements(exe.options.env);
125+
126+
return exe;
127+
}
128+
129+
/*
130+
This creates a sufficiently unique name for a docker container that won't
131+
conflict with other containers on a system, but known enough for us to find
132+
it if we lose track of it somehow
133+
*/
134+
private createUniqueDockerName() {
135+
return 'puppet-vscode-xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
136+
const r = (Math.random() * 16) | 0,
137+
v = c === 'x' ? r : (r & 0x3) | 0x8;
138+
return v.toString(16);
139+
});
140+
}
141+
142+
/*
143+
This uses docker to query what random port was assigned the container we
144+
created, and a regex to parse the port number out of the result
145+
*/
146+
private getDockerPort(name: string) {
147+
let cmd: string = this.getDockerCommand(process.platform);
148+
let args: Array<string> = ['port', name, '8082'];
149+
var proc = cp.spawnSync(cmd, args);
150+
let regex = /:(\d+)$/m;
151+
return Number(regex.exec(proc.stdout.toString())[1]);
152+
}
153+
154+
// this stops and removes docker containers forcibly
155+
private stopLanguageServerDockerProcess(name: string): void {
156+
let cmd: string = this.getDockerCommand(process.platform);
157+
let args: Array<string> = ['rm', '--force', name];
158+
let spawn_options: cp.SpawnOptions = {};
159+
spawn_options.stdio = 'pipe';
160+
cp.spawn(cmd, args, spawn_options);
161+
}
162+
163+
// platform specific docker command
164+
private getDockerCommand(platform: string): string {
165+
switch (platform) {
166+
case 'win32':
167+
return 'docker.exe';
168+
default:
169+
return 'docker';
170+
}
171+
}
172+
173+
// docker specific arguments to start the container how we need it started
174+
private getDockerArguments(containerName: string, imageName: string) {
175+
let args = [
176+
'run', // run a new container
177+
'--rm', // automatically remove container when it exits
178+
'-i', // interactive
179+
'-P', // publish all exposed ports to random ports
180+
'--name',
181+
containerName, // assign a name to the container
182+
imageName, // image to use
183+
];
184+
return args;
185+
}
186+
}

0 commit comments

Comments
 (0)