Skip to content

Commit

Permalink
Merge pull request #627 from hatched/poll-on-add
Browse files Browse the repository at this point in the history
Connect to and start polling models when registering a new controller
  • Loading branch information
barrymcgee authored Jul 29, 2020
2 parents e332f41 + 2b5aaef commit 7b3a0c5
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 94 deletions.
137 changes: 85 additions & 52 deletions src/app/model-poller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import {
fetchControllerList,
loginWithBakery,
} from "juju";
import { fetchModelList } from "juju/actions";

import {
storeLoginError,
updateControllerConnection,
updateJujuAPIInstance,
updatePingerIntervalId,
} from "app/actions";

import { updateModelList } from "juju/actions";

import {
getConfig,
getControllerData,
getUserPass,
getWSControllerURL,
isLoggedIn,
Expand All @@ -29,6 +33,7 @@ export default async function connectAndListModels(
const { identityProviderAvailable, isJuju } = getConfig(storeState);
const wsControllerURL = getWSControllerURL(storeState);
const credentials = getUserPass(wsControllerURL, storeState);
const controllers = getControllerData(storeState) || {};
const defaultControllerData = [
wsControllerURL,
credentials,
Expand All @@ -39,58 +44,14 @@ export default async function connectAndListModels(
if (additionalControllers) {
controllerList = controllerList.concat(additionalControllers);
}
controllerList.forEach(async (controllerData) => {
let conn, error, juju, intervalId;
try {
({ conn, error, juju, intervalId } = await loginWithBakery(
...controllerData
));
if (error) {
reduxStore.dispatch(storeLoginError(error));
return;
}
} catch (e) {
return console.log("unable to log into controller", e, controllerData);
}

// XXX Now that we can register multiple controllers this needs
// to be set per controller.
if (process.env.NODE_ENV === "production") {
Sentry.setTag("jujuVersion", conn?.info?.serverVersion);
}

reduxStore.dispatch(updateControllerConnection(controllerData[0], conn));
reduxStore.dispatch(updateJujuAPIInstance(controllerData[0], juju));
reduxStore.dispatch(
updatePingerIntervalId(controllerData[0], intervalId)
);

fetchControllerList(
controllerData[0],
conn,
controllerData[4],
reduxStore
);
// XXX the isJuju Check needs to be done on a per-controller basis
if (!isJuju) {
// This call will be a noop if the user isn't an administrator
// on the JIMM controller we're connected to.
disableControllerUUIDMasking(conn);
}

do {
await reduxStore.dispatch(fetchModelList(conn), {
wsControllerURL: controllerData[0],
});
await fetchAllModelStatuses(controllerData[0], conn, reduxStore);
// Wait 30s then start again.
await new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 30000);
});
} while (isLoggedIn(controllerData[0], reduxStore.getState()));
const connectedControllers = Object.keys(controllers);
controllerList = controllerList.filter((controllerData) => {
// remove controllers we're already connected to.
return !connectedControllers.includes(controllerData[0]);
});
controllerList.forEach((controllerData) =>
connectAndPollController(controllerData, isJuju, reduxStore)
);
} catch (error) {
// XXX Surface error to UI.
// XXX Send to sentry if it's an error that's not connection related
Expand All @@ -99,3 +60,75 @@ export default async function connectAndListModels(
console.error("Something went wrong: ", error);
}
}

/**
@param {Object} controllerData The data to use to connect to the controller.
In the format [
wsControllerURL - The fully qualified controller url wss://ip:port/api
credentials - An object with the keys {user, password}
bakery - An instance of the bakery to use if necessary
identityProviderAvailable - If an identity provider is to be used. If so
a bakery must be provided.
]
@param {Boolean} isJuju
@param {Object} reduxStore
*/
export async function connectAndPollController(
controllerData,
isJuju,
reduxStore
) {
let conn, error, juju, intervalId;
try {
({ conn, error, juju, intervalId } = await loginWithBakery(
...controllerData
));
if (error) {
reduxStore.dispatch(storeLoginError(error));
return;
}
} catch (e) {
return console.log("unable to log into controller", e, controllerData);
}

// XXX Now that we can register multiple controllers this needs
// to be sent per controller.
if (process.env.NODE_ENV === "production") {
Sentry.setTag("jujuVersion", conn?.info?.serverVersion);
}

reduxStore.dispatch(updateControllerConnection(controllerData[0], conn));
reduxStore.dispatch(updateJujuAPIInstance(controllerData[0], juju));
reduxStore.dispatch(updatePingerIntervalId(controllerData[0], intervalId));

fetchControllerList(controllerData[0], conn, controllerData[4], reduxStore);
// XXX the isJuju Check needs to be done on a per-controller basis
if (!isJuju) {
// This call will be a noop if the user isn't an administrator
// on the JIMM controller we're connected to.
disableControllerUUIDMasking(conn);
}

do {
const models = await conn.facades.modelManager.listModels({
tag: conn.info.user.identity,
});
reduxStore.dispatch(updateModelList(models), {
wsControllerURL: controllerData[0],
});
const modelUUIDList = models.userModels.map((item) => item.model.uuid);
await fetchAllModelStatuses(
controllerData[0],
modelUUIDList,
conn,
reduxStore
);
// Wait 30s then start again.
await new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 30000);
});
} while (isLoggedIn(controllerData[0], reduxStore.getState()));
}
14 changes: 2 additions & 12 deletions src/app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,14 @@ export const getModelData = (state) => {
@param {Object} state The application state.
@returns {Object|Null} The list of controller data or null if none found.
*/
export const getControllerData = (state) => {
if (state.juju && state.juju.controllers) {
return state.juju.controllers;
}
return null;
};
export const getControllerData = (state) => state?.juju?.controllers;

/**
Fetches the bakery from state.
@param {Object} state The application state.
@returns {Object|Null} The bakery instance or null if none found.
*/
export const getBakery = (state) => {
if (state.root && state.root.bakery) {
return state.root.bakery;
}
return null;
};
export const getBakery = (state) => state?.root?.bakery;

/**
Fetches the application config from state.
Expand Down
18 changes: 0 additions & 18 deletions src/juju/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { fetchAndStoreModelStatus } from "juju";
// Action labels
export const actionsList = {
clearModelData: "CLEAR_MODEL_DATA",
fetchModelList: "FETCH_MODEL_LIST",
updateControllerList: "UPDATE_CONTROLLER_LIST",
updateModelInfo: "UPDATE_MODEL_INFO",
updateModelStatus: "UPDATE_MODEL_STATUS",
Expand Down Expand Up @@ -71,23 +70,6 @@ export function updateModelInfo(modelInfo) {

// Thunks

/**
Fetches the model list from the supplied Juju controller. Requires that the
user is logged in to dispatch the retrieved data from listModels.
@param {Object} conn The controller connection.
@returns {Object} models The list of model objects under the key `userModels`.
*/
export function fetchModelList(conn) {
return async function fetchModelList(dispatch, getState) {
const models = await conn.facades.modelManager.listModels({
tag: conn.info.user.identity,
});
dispatch(updateModelList(models), {
wsControllerURL: conn.transport._ws.url,
});
};
}

/**
Returns the model status that's stored in the database if it exists or makes
another call to request it if it doesn't.
Expand Down
12 changes: 8 additions & 4 deletions src/juju/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,17 +238,21 @@ async function fetchModelInfo(conn, modelUUID) {
Loops through each model UUID to fetch the status. Uppon receiving the status
dispatches to store that status data.
@param {Object} conn The connection to the controller.
@param {Array} modelUUIDList A list of the model uuid's to connect to.
@param {Object} reduxStore The applications reduxStore.
@returns {Promise} Resolves when the queue fetching the model statuses has
completed. Does not reject.
*/
export async function fetchAllModelStatuses(wsControllerURL, conn, reduxStore) {
export async function fetchAllModelStatuses(
wsControllerURL,
modelUUIDList,
conn,
reduxStore
) {
const getState = reduxStore.getState;
const modelList = getState().juju.models;
const dispatch = reduxStore.dispatch;
const queue = new Limiter({ concurrency: 5 });
const modelUUIDs = Object.keys(modelList);
modelUUIDs.forEach((modelUUID) => {
modelUUIDList.forEach((modelUUID) => {
queue.push(async (done) => {
if (isLoggedIn(wsControllerURL, getState())) {
await fetchAndStoreModelStatus(
Expand Down
18 changes: 10 additions & 8 deletions src/pages/Controllers/Controllers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { useDispatch, useSelector, useStore } from "react-redux";
import cloneDeep from "clone-deep";
import classNames from "classnames";

Expand All @@ -8,7 +8,8 @@ import Header from "components/Header/Header";
import SlidePanel from "components/SlidePanel/SlidePanel";
import MainTable from "@canonical/react-components/dist/components/MainTable/MainTable";

import { getControllerData, getModelData } from "app/selectors";
import { getBakery, getControllerData, getModelData } from "app/selectors";
import { connectAndStartPolling } from "app/actions";
import useLocalStorage from "hooks/useLocalStorage";
import ControllersOverview from "./ControllerOverview/ControllerOverview";

Expand Down Expand Up @@ -151,6 +152,9 @@ function Details() {

function RegisterAController({ onClose }) {
const [formValues, setFormValues] = useState({});
const dispatch = useDispatch();
const reduxStore = useStore();
const bakery = useSelector(getBakery);
const [additionalControllers, setAdditionalControllers] = useLocalStorage(
"additionalControllers",
[]
Expand All @@ -167,6 +171,7 @@ function RegisterAController({ onClose }) {
true, // additional controller
]);
setAdditionalControllers(additionalControllers);
dispatch(connectAndStartPolling(reduxStore, bakery));
onClose(); // Close the SlidePanel
}

Expand Down Expand Up @@ -218,7 +223,7 @@ function RegisterAController({ onClose }) {
id="full-name-stacked"
name="controllerName"
onChange={handleInputChange}
required="true"
required={true}
/>
<p className="p-form-help-text">
Must be a valid alpha-numeric Juju controller name. <br />
Expand All @@ -245,7 +250,7 @@ function RegisterAController({ onClose }) {
id="full-name-stacked"
name="wsControllerHost"
onChange={handleInputChange}
required="true"
required={true}
/>
<p className="p-form-help-text">
You'll typically want to use the public IP:Port address for the
Expand All @@ -270,7 +275,6 @@ function RegisterAController({ onClose }) {
id="full-name-stacked"
name="username"
onChange={handleInputChange}
required=""
/>
<p className="p-form-help-text">
The username you use to access the controller.
Expand All @@ -293,7 +297,6 @@ function RegisterAController({ onClose }) {
id="full-name-stacked"
name="password"
onChange={handleInputChange}
required=""
/>
<p className="p-form-help-text">
The password will be what you used when running{" "}
Expand All @@ -316,7 +319,6 @@ function RegisterAController({ onClose }) {
name="identityProvider"
defaultChecked={false}
onChange={handleInputChange}
required=""
/>
<label htmlFor="identityProviderAvailable">
An identity provider is available.{" "}
Expand Down Expand Up @@ -345,7 +347,7 @@ function RegisterAController({ onClose }) {
name="certificateAccepted"
defaultChecked={false}
onChange={handleInputChange}
required="true"
required={true}
/>
<label htmlFor="certificateHasBeenAccepted">
The SSL certificate, if any, has been accepted.{" "}
Expand Down

0 comments on commit 7b3a0c5

Please sign in to comment.