Skip to content

Commit 16e2cdc

Browse files
committed
Merge branch 'master' into trevj-cloud-social-polish
2 parents c944fb9 + 0c1d8d9 commit 16e2cdc

File tree

7 files changed

+173
-13
lines changed

7 files changed

+173
-13
lines changed

Gruntfile.coffee

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ config =
6565
libsForDeployerChromeApp:
6666
Rule.copyLibs
6767
npmLibNames: ['freedom-for-chrome', 'forge-min']
68-
pathsFromDevBuild: ['loggingprovider', 'cloud/deployer', 'cloud/digitalocean']
68+
pathsFromDevBuild: ['loggingprovider', 'cloud/deployer', 'cloud/digitalocean', 'cloud/install']
6969
localDestPath: 'samples/deployer-chromeapp/'
7070
libsForDeployerFirefoxApp:
7171
Rule.copyLibs
7272
npmLibNames: ['freedom-for-firefox', 'forge-min']
73-
pathsFromDevBuild: ['loggingprovider', 'cloud/deployer', 'cloud/digitalocean']
73+
pathsFromDevBuild: ['loggingprovider', 'cloud/deployer', 'cloud/digitalocean', 'cloud/install']
7474
localDestPath: 'samples/deployer-firefoxapp/data'
7575

7676
libsForZorkChromeApp:
@@ -299,11 +299,20 @@ config =
299299
ignore: ['ws', 'path']
300300
browserifyOptions: { standalone: 'browserified_exports' }
301301
})
302-
digitalOceanFreedomModule: Rule.browserify 'cloud/digitalocean/freedom-module'
303-
# Sample app freedom modules.
304-
copypasteChatFreedomModule: Rule.browserify 'copypaste-chat/freedom-module'
305-
copypasteSocksFreedomModule: Rule.browserify 'copypaste-socks/freedom-module'
306-
echoServerFreedomModule: Rule.browserify 'echo/freedom-module'
302+
# TODO: Make the browserified SSH stuff re-useable, e.g. freedomjs module.
303+
cloudInstallerFreedomModule: Rule.browserify('cloud/install/freedom-module', {
304+
alias : [
305+
# Shims for node's dns and net modules from freedom-social-xmpp,
306+
# with a couple of fixes.
307+
'./src/cloud/social/shim/net.js:net'
308+
'./src/cloud/social/shim/dns.js:dns'
309+
# Subset of ssh2-streams (all except SFTP) which works well in
310+
# the browser.
311+
'./src/cloud/social/alias/ssh2-streams.js:ssh2-streams'
312+
# Fallback for crypto-browserify's randombytes, for Firefox.
313+
'./src/cloud/social/alias/randombytes.js:randombytes'
314+
]
315+
})
307316
cloudSocialProviderFreedomModule: Rule.browserify('cloud/social/freedom-module', {
308317
alias : [
309318
# Shims for node's dns and net modules from freedom-social-xmpp,
@@ -317,7 +326,12 @@ config =
317326
'./src/cloud/social/alias/randombytes.js:randombytes'
318327
]
319328
})
329+
digitalOceanFreedomModule: Rule.browserify 'cloud/digitalocean/freedom-module'
330+
# Sample app freedom modules.
331+
copypasteChatFreedomModule: Rule.browserify 'copypaste-chat/freedom-module'
332+
copypasteSocksFreedomModule: Rule.browserify 'copypaste-socks/freedom-module'
320333
deployerFreedomModule: Rule.browserify 'cloud/deployer/freedom-module'
334+
echoServerFreedomModule: Rule.browserify 'echo/freedom-module'
321335
simpleChatFreedomModule: Rule.browserify 'simple-chat/freedom-module'
322336
simpleSocksFreedomModule: Rule.browserify 'simple-socks/freedom-module'
323337
simpleTurnFreedomModule: Rule.browserify 'simple-turn/freedom-module'
@@ -432,6 +446,7 @@ taskManager.add 'base', [
432446
'ts:srcInCoreEnv'
433447
'browserify:loggingProvider'
434448
'browserify:churnPipeFreedomModule'
449+
'browserify:cloudInstallerFreedomModule'
435450
'browserify:cloudSocialProviderFreedomModule'
436451
'browserify:digitalOceanFreedomModule'
437452
]

src/cloud/deployer/freedom-module.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"digitalocean": {
1919
"url": "../digitalocean/freedom-module.json",
2020
"api": "digitalocean"
21+
},
22+
"cloudinstall": {
23+
"url": "../install/freedom-module.json",
24+
"api": "cloudinstall"
2125
}
2226
}
2327
}

src/cloud/deployer/freedom-module.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ loggingController.setDefaultFilter(loggingTypes.Destination.console,
1010

1111
const log :logging.Log = new logging.Log('deployer');
1212

13-
const digitalOcean = freedom['digitalocean']();
13+
const provisioner = freedom['digitalocean']();
1414

15-
digitalOcean.on('status', (msg:any) => {
15+
provisioner.on('status', (msg:any) => {
1616
log.info('status: %1', msg.message);
1717
});
1818

1919
log.info('deploying...');
20+
// TODO: typings for provisioner return type
21+
provisioner.start('test').then((serverInfo: any) => {
22+
log.info('server provisioned: %1', serverInfo);
2023

21-
digitalOcean.start('test').then((ret: any) => {
22-
log.info('final result: %1', ret);
24+
const installer = freedom['cloudinstall']();
25+
log.info('installing...');
26+
installer.install(serverInfo.network.ipv4, serverInfo.network.ssh_port,
27+
'root', serverInfo.ssh.private).then((invitation: string) => {
28+
log.info('uproxy installed! invitation: %1', invitation);
29+
}, (e: Error) => {
30+
log.error('failed to install: %1', e.message);
31+
});
2332
}, (e:Error) => {
24-
log.error('failed to deploy: %1', e);
33+
log.error('failed to deploy: %1', e.message);
2534
});

src/cloud/digitalocean/provisioner.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ class Provisioner {
256256
* Waits for all in-progress Digital Ocean actions to complete
257257
* e.g. after powering on a machine, or creating a VM
258258
*/
259+
// TODO: It's not until several moments after this resolves
260+
// that you can actually SSH into the server. We need
261+
// to find a way to detect when the machine is *really*
262+
// ready.
259263
private waitDigitalOceanActions_ = () : Promise<void> => {
260264
console.log("Polling for Digital Ocean in-progress actions");
261265
return this.doRequest_("GET", "droplets/" + this.state_.cloud.vm.id + "/actions").then((resp: any) => {
@@ -322,7 +326,12 @@ class Provisioner {
322326
name: name,
323327
region: region,
324328
size: "512mb",
325-
image: "ubuntu-14-04-x64",
329+
// 'docker' is a slug name, a.k.a. application image, a.k.a. one-click app.
330+
// The full list of available slugs is available only through the API, e.g.:
331+
// this.doRequest_("GET", "images?type=application&per_page=50").then((resp: any) => {
332+
// console.log('available application images: ' + JSON.stringify(resp, undefined, 2));
333+
// });
334+
image: 'docker',
326335
ssh_keys: [ this.state_.cloud.ssh.id ]
327336
}));
328337
// If missing, create the droplet
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "Cloud Installer",
3+
"description": "Installs uProxy in the cloud, via SSH.",
4+
"app": {
5+
"script": [
6+
"freedom-module.static.js"
7+
]
8+
},
9+
"provides": [ "cloudinstall" ],
10+
"default": "cloudinstall",
11+
"api": {
12+
"cloudinstall": {
13+
"install": {
14+
"type": "method",
15+
"value": ["string", "number", "string", "string"],
16+
"ret": "string"
17+
},
18+
"err": {
19+
"message": "string"
20+
}
21+
}
22+
},
23+
"permissions": [
24+
"core.tcpsocket"
25+
]
26+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference path='../../../../third_party/typings/freedom/freedom-module-env.d.ts' />
2+
3+
import CloudInstaller = require('./installer');
4+
5+
freedom().providePromises(CloudInstaller);
6+
7+
export = CloudInstaller

src/cloud/install/installer.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/// <reference path='../../../../third_party/typings/es6-promise/es6-promise.d.ts' />
2+
/// <reference path='../../../../third_party/typings/freedom/freedom-module-env.d.ts' />
3+
/// <reference path='../../../../third_party/typings/node/node.d.ts' />
4+
/// <reference path='../../../../third_party/typings/ssh2/ssh2.d.ts' />
5+
6+
import logging = require('../../logging/logging');
7+
8+
// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
9+
import * as ssh2 from 'ssh2';
10+
var Client = require('ssh2').Client;
11+
12+
var log: logging.Log = new logging.Log('cloud installer');
13+
14+
// Command to install uProxy.
15+
const INSTALL_COMMAND = 'curl -sSL https://raw.githubusercontent.com/uProxy/uproxy-docker/master/install-cloud.sh | sh';
16+
17+
// Prefix for invitation URLs.
18+
const INVITATION_URL_PREFIX = 'https://www.uproxy.org/invite/';
19+
20+
// Installs uProxy on a server, via SSH.
21+
// The process is as close as possible to a manual install
22+
// so that we have fewer paths to test.
23+
class CloudInstaller {
24+
constructor(private dispatchEvent_: (name: string, args: Object) => void) {}
25+
26+
// Runs the install command via SSH, resolving with the invitation URL.
27+
public install = (
28+
host:string,
29+
port:number,
30+
username:string,
31+
key:string) : Promise<string> => {
32+
log.debug('installing uproxy on %1:%2 as %3', host, port, username);
33+
34+
const connectConfig: ssh2.ConnectConfig = {
35+
host: host,
36+
port: port,
37+
username: username,
38+
privateKey: key,
39+
// Remaining fields only for type-correctness.
40+
tryKeyboard: false,
41+
debug: undefined
42+
};
43+
44+
const connection = new Client();
45+
return new Promise<string>((F, R) => {
46+
connection.on('ready', () => {
47+
log.debug('logged into server');
48+
connection.exec(INSTALL_COMMAND, (e: Error, stream: ssh2.Channel) => {
49+
if (e) {
50+
connection.end();
51+
R({
52+
message: 'could not execute command: ' + e.message
53+
});
54+
return;
55+
}
56+
stream.on('end', () => {
57+
connection.end();
58+
R({
59+
message: 'invitation URL not found'
60+
});
61+
}).on('data', function(data: Buffer) {
62+
const output = data.toString();
63+
log.debug('STDOUT: %1', output);
64+
// Search for the URL anywhere in the line so we will
65+
// continue to work in the face of minor changes
66+
// to the install script.
67+
if (output.indexOf(INVITATION_URL_PREFIX) === 0) {
68+
F(output.substring(INVITATION_URL_PREFIX.length));
69+
}
70+
}).stderr.on('data', function(data: Buffer) {
71+
log.error(data.toString());
72+
});
73+
});
74+
}).on('error', (e: Error) => {
75+
// This occurs when:
76+
// - user supplies the wrong username or password
77+
// - host cannot be reached, e.g. non-existant hostname
78+
R({
79+
message: 'could not login: ' + e.message
80+
});
81+
}).on('end', () => {
82+
log.debug('connection end');
83+
}).on('close', (hadError: boolean) => {
84+
log.debug('connection close, with%1 error', (hadError ? '' : 'out'));
85+
}).connect(connectConfig);
86+
});
87+
}
88+
}
89+
90+
export = CloudInstaller;

0 commit comments

Comments
 (0)