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

Fix API errors when handling nil pointers to structs. #14

Merged
merged 2 commits into from
Jul 26, 2018
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
75 changes: 74 additions & 1 deletion api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,79 @@ function connect(url, options={}, callback) {
}


/**
Connect to the Juju controller or model at the given URL and the authenticate
using the given credentials.

@param {String} url The WebSocket URL of the Juju controller or model.
@param {Object} credentials An object with the user and password fields for
userpass authentication or the macaroons field for bakery authentication.
If an empty object is provided a full bakery discharge will be attempted
for logging in with macaroons. Any necessary third party discharges are
performed using the bakery instance provided in the options (see below).
@param {Object} options Connections options, including:
- facades (default=[]): the list of facade classes to include in the API
connection object. Those classes are usually auto-generated and can be
found in the facades directory of the project. When multiple versions of
the same facade are included, the most recent version supported by the
server is made available as part of the connection object;
- debug (default=false): when enabled, all API messages are logged at debug
level;
- wsclass (default=W3C browser native WebSocket): the WebSocket class to use
for opening the connection and sending/receiving messages. Server side,
require('websocket').w3cwebsocket can be used safely, as it implements the
W3C browser native WebSocket API;
- bakery (default: null): the bakery client to use when macaroon discharges
are required, in the case an external user is used to connect to Juju;
see <https://www.npmjs.com/package/macaroon-bakery>;
- closeCallback: a callback to be called with the exit code when the
connection is closed.
@param {Function} callback Called when the login process completes, the
callback receives an error, a connection object and a logout function that
can be used to clos ethe connection. If there are no errors, the connection
can be used to send/receive messages to and from the Juju controller or
model, and to get access to the available facades (through conn.facades).
See the docstring for the Connection class for information on how to use
the connection instance. Redirection errors are automatically handled by
this function, so any error is a real connection problem.
*/
function connectAndLogin(url, credentials, options, callback) {
// Connect to Juju.
connect(url, options, (err, juju) => {
if (err) {
callback(err, null, null);
return;
}
// Authenticate.
juju.login(credentials, (err, conn) => {
if (!err) {
callback(null, conn, juju.logout.bind(juju));
return;
}
if (!juju.isRedirectionError(err)) {
callback(err, null, null);
return;
}
// Redirect to the real model.
juju.logout();
for (let i = 0; i < err.servers.length; i++) {
const srv = err.servers[i];
// TODO(frankban): we should really try to connect to all servers and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create an issue for this task so that we don't forget about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: #15

// just use the first connection available, without second guessing
// that the public hostname is reachable.
if (srv.type === 'hostname' && srv.scope === 'public') {
// This is a public server with a dns-name, connect to it.
connectAndLogin(srv.url(url), credentials, options, callback);
return;
}
}
callback(
new Error('cannot connect to model after redirection'), null, null);
});
});
}


/**
A Juju API client allowing for logging in and get access to facades.

Expand Down Expand Up @@ -392,4 +465,4 @@ function uncapitalize(string) {
}


module.exports = {connect: connect};
module.exports = {connect, connectAndLogin};
126 changes: 126 additions & 0 deletions api/test-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,16 @@ tap.test('connect', t => {
t.equal(
err.servers[0].url('srv1-uuid'),
'wss://1.2.3.4:17070/model/srv1-uuid/api');
t.equal(
err.servers[0].url('wss://4.3.2.1:443/model/uuid-in-url/api'),
'wss://1.2.3.4:17070/model/uuid-in-url/api');
t.equal(err.servers[1].value, 'example.com');
t.equal(err.servers[1].port, 443);
t.equal(err.servers[1].type, 'hostname');
t.equal(err.servers[1].scope, 'global');
t.equal(
err.servers[1].url('ws://4.3.2.1:443/model/another/api'),
'wss://example.com:443/model/another/api');
t.equal(
err.servers[1].url('srv2-uuid'),
'wss://example.com:443/model/srv2-uuid/api');
Expand Down Expand Up @@ -299,3 +305,123 @@ tap.test('connect', t => {

t.end();
});


tap.test('connectAndLogin', t => {
let ws;
const url = 'wss://1.2.3.4';
const options = {
wsclass: helpers.makeWSClass(instance => {
ws = instance;
})
};

t.test('connect failure', t => {
const creds = {};
jujulib.connectAndLogin(url, creds, options, (err, conn, logout) => {
t.equal(err, 'cannot connect WebSocket: bad wolf');
t.equal(conn, null);
t.equal(logout, null);
t.end();
});
// Close the WebSocket connection.
ws.close('bad wolf');
});

t.test('login failure', t => {
const creds = {user: 'who', password: 'tardis'};
jujulib.connectAndLogin(url, creds, options, (err, conn, logout) => {
helpers.requestEqual(t, ws.lastRequest, {
type: 'Admin',
request: 'Login',
params: {'auth-tag': 'who', credentials: 'tardis', macaroons: []},
version: 3
});
t.equal(err, 'bad wolf');
t.equal(conn, null);
t.equal(logout, null);
t.end();
});
// Open the WebSocket connection.
ws.open();
// Reply to the login request.
ws.reply({error: 'bad wolf'});
});

t.test('login redirection error failure', t => {
const creds = {user: 'who', password: 'tardis'};
jujulib.connectAndLogin(url, creds, options, (err, conn, logout) => {
helpers.requestEqual(t, ws.lastRequest, {
type: 'Admin',
request: 'RedirectInfo',
params: {},
version: 3
});
t.equal(err, 'bad wolf');
t.equal(conn, null);
t.equal(logout, null);
t.end();
});
// Open the WebSocket connection.
ws.open();
// Reply to the login request.
ws.reply({error: 'redirection required'});
// Reply to the redirectInfo request.
ws.reply({error: 'bad wolf'});
});

t.test('login redirection error success', t => {
const creds = {user: 'who', password: 'tardis'};
jujulib.connectAndLogin(url, creds, options, (err, conn, logout) => {
t.equal(err, null);
t.notEqual(conn, null);
t.notEqual(logout, null);
logout();
// The WebSocket is now closed.
t.equal(ws.readyState, 3);
t.end();
});
// Open the WebSocket connection.
ws.open();
// Reply to the login request.
ws.reply({error: 'redirection required'});
// Reply to the redirectInfo request.
ws.reply({response: {
'ca-cert': 'mycert',
'servers': [[{
value: '1.2.3.4',
port: 17070,
type: 'ipv4',
scope: 'public'
}, {
value: 'example.com',
port: 443,
type: 'hostname',
scope: 'public'
}]]
}});
// Open the new WebSocket connection.
ws.open();
// Reply to the new login request.
ws.reply({});
});

t.test('login success', t => {
const creds = {user: 'who', password: 'tardis'};
jujulib.connectAndLogin(url, creds, options, (err, conn, logout) => {
t.equal(err, null);
t.notEqual(conn, null);
t.notEqual(logout, null);
logout();
// The WebSocket is now closed.
t.equal(ws.readyState, 3);
t.end();
});
// Open the WebSocket connection.
ws.open();
// Reply to the login request.
ws.reply({});
});

t.end();
});
5 changes: 5 additions & 0 deletions api/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ const loginResponse = {
'model-access': 'admin'
},
facades: [{
name: 'AllModelWatcher', versions: [1]
}, {
name: 'AllWatcher', versions: [0]
}, {
name: 'Application', versions: [7]
}, {
name: 'Client', versions: [2, 3]
}, {
name: 'Controller', versions: [3, 4, 5]
}, {
name: 'MyFacade', versions: [1, 7]
}]
Expand Down Expand Up @@ -109,6 +113,7 @@ class WebSocket {
}

close(reason) {
this.readyState = 3;
this.onclose({reason: reason});
}

Expand Down
Loading