feat(@embark/api): Add command `service api on/off`

Add support for `service api on/off` commands.

Deprecate commands `api start/stop` in favor of `service api on/off`.

`service api on` - Enables the API server serving Cockpit. Shows an error if the API server is already starting or started.

`service api off` - Disables the API server serving Cockpit. Shows an error if the API server is already stopping or stopped.

`api start` - This command has been deprecated in favor of `service api on` and will be removed in future versions.

`api stop` - This command has been deprecated in favor of `service api off` and will be removed in future versions.

`api:start` - This event has been deprecated and will be removed in future versions.

`api:stop` - This event has been deprecated and will be removed in future versions.
This commit is contained in:
emizzle 2019-05-01 15:59:07 +10:00 committed by Pascal Precht
parent 3a07d34cc2
commit 634feb597a
6 changed files with 109 additions and 60 deletions

View File

@ -11,6 +11,7 @@ export default class Api {
private port!: number; private port!: number;
private api!: Server; private api!: Server;
private apiUrl!: string; private apiUrl!: string;
private isServiceRegistered = false;
constructor(private embark: Embark, private options: any) { constructor(private embark: Embark, private options: any) {
this.embark.events.emit("status", __("Starting API & Cockpit UI")); this.embark.events.emit("status", __("Starting API & Cockpit UI"));
@ -22,7 +23,11 @@ export default class Api {
this.listenToCommands(); this.listenToCommands();
this.registerConsoleCommands(); this.registerConsoleCommands();
this.init();
});
}
private init() {
this.embark.events.request("processes:register", "api", { this.embark.events.request("processes:register", "api", {
launchFn: (cb: (error: Error | null, message: string) => void) => { launchFn: (cb: (error: Error | null, message: string) => void) => {
this.api.start() this.api.start()
@ -44,10 +49,20 @@ export default class Api {
} }
this.setServiceCheck(); this.setServiceCheck();
}); });
this.embark.events.on("check:wentOffline:api", () => {
this.embark.logger.info(__("Cockpit is offline, please close Cockpit."));
});
this.embark.events.on("check:backOnline:api", () => {
this.embark.logger.info(__("Cockpit is online, please open/refresh Cockpit."));
}); });
} }
private setServiceCheck() { private setServiceCheck() {
if (this.isServiceRegistered) {
return;
}
this.isServiceRegistered = true;
this.embark.events.request("services:register", "api", (cb: (options: object) => any) => { this.embark.events.request("services:register", "api", (cb: (options: object) => any) => {
checkIsAvailable(this.apiUrl, (isAvailable: boolean) => { checkIsAvailable(this.apiUrl, (isAvailable: boolean) => {
const devServer = __("Cockpit UI") + " (" + this.apiUrl + ")"; const devServer = __("Cockpit UI") + " (" + this.apiUrl + ")";
@ -55,16 +70,18 @@ export default class Api {
return cb({ name: devServer, status: serverStatus }); return cb({ name: devServer, status: serverStatus });
}); });
}); });
this.embark.events.on("check:wentOffline:api", () => {
this.embark.logger.info(__("Cockpit UI is offline"));
});
} }
private listenToCommands() { private listenToCommands() {
this.embark.events.setCommandHandler("api:url", (cb) => cb(this.apiUrl)); this.embark.events.setCommandHandler("api:url", (cb) => cb(this.apiUrl));
this.embark.events.setCommandHandler("api:start", (cb) => this.embark.events.request("processes:launch", "api", cb)); this.embark.events.setCommandHandler("api:start", (cb) => {
this.embark.events.setCommandHandler("api:stop", (cb) => this.embark.events.request("processes:stop", "api", cb)); this.embark.logger.warn(__("The event 'api:start' has been deprecated and will be removed in future versions."));
this.embark.events.request("processes:launch", "api", cb);
});
this.embark.events.setCommandHandler("api:stop", (cb) => {
this.embark.logger.warn(__("The event 'api:stop' has been deprecated and will be removed in future versions."));
this.embark.events.request("processes:stop", "api", cb);
});
this.embark.events.setCommandHandler("logs:api:enable", (cb) => { this.embark.events.setCommandHandler("logs:api:enable", (cb) => {
this.api.enableLogging(); this.api.enableLogging();
cb(); cb();
@ -79,16 +96,24 @@ export default class Api {
this.embark.registerConsoleCommand({ this.embark.registerConsoleCommand({
description: __("Start or stop the API"), description: __("Start or stop the API"),
matches: ["api start"], matches: ["api start"],
process: (cmd: string, callback: () => void) => { process: (cmd: string, callback: (msg: string) => void) => {
this.embark.events.request("api:start", callback); const message = __("The command 'api:start' has been deprecated in favor of 'service api on' and will be removed in future versions.");
this.embark.logger.warn(message); // logs to Embark's console
this.embark.events.request("processes:launch", "api", (err: string, msg: string) => {
callback(err || msg); // logs to Cockpit's console
});
}, },
usage: "api start/stop", usage: "api start/stop",
}); });
this.embark.registerConsoleCommand({ this.embark.registerConsoleCommand({
matches: ["api stop"], matches: ["api stop"],
process: (cmd: string, callback: () => void) => { process: (cmd: string, callback: (msg: string) => void) => {
this.embark.events.request("api:stop", callback); const message = __("The command 'api:stop' has been deprecated in favor of 'service api off' and will be removed in future versions.");
this.embark.logger.warn(message); // logs to Embark's console
this.embark.events.request("processes:stop", "api", (err: string, msg: string) => {
callback(err || msg); // logs to Cockpit's console
});
}, },
}); });

View File

@ -9,6 +9,7 @@ import expressWs from "express-ws";
import findUp from "find-up"; import findUp from "find-up";
import helmet from "helmet"; import helmet from "helmet";
import * as http from "http"; import * as http from "http";
import * as net from "net";
import * as path from "path"; import * as path from "path";
import * as ws from "ws"; import * as ws from "ws";
@ -28,6 +29,8 @@ export default class Server {
private isLogging: boolean = false; private isLogging: boolean = false;
private server?: http.Server; private server?: http.Server;
private openSockets = new Set<net.Socket>();
constructor(private embark: Embark, private port: number, private hostname: string, private plugins: Plugins) { constructor(private embark: Embark, private port: number, private hostname: string, private plugins: Plugins) {
this.expressInstance = this.initApp(); this.expressInstance = this.initApp();
this.embarkUiBuildDir = (findUp.sync("node_modules/embark-ui/build", {cwd: embarkPath()}) || embarkPath("node_modules/embark-ui/build")); this.embarkUiBuildDir = (findUp.sync("node_modules/embark-ui/build", {cwd: embarkPath()}) || embarkPath("node_modules/embark-ui/build"));
@ -67,6 +70,15 @@ export default class Server {
this.server = this.expressInstance.app.listen(this.port, this.hostname, () => { this.server = this.expressInstance.app.listen(this.port, this.hostname, () => {
resolve(); resolve();
}); });
// keep track of our open websockets so we can destroy them
// if the api server is shutdown
this.server.on("connection", (socket) => {
this.openSockets.add(socket);
socket.on("close", () => {
this.openSockets.delete(socket);
});
});
}); });
} }
@ -77,6 +89,9 @@ export default class Server {
return reject(new Error(message)); return reject(new Error(message));
} }
// close any open sockets
this.openSockets.forEach((socket) => socket.destroy());
this.server.close(() => { this.server.close(() => {
this.server = undefined; this.server = undefined;
resolve(); resolve();

View File

@ -1,4 +1,5 @@
export interface Logger { export interface Logger {
info(text: string): void; info(text: string): void;
warn(text: string): void;
error(text: string, ...args: Array<string|Error>): void; error(text: string, ...args: Array<string|Error>): void;
} }

View File

@ -8,6 +8,8 @@ import Contracts from '../components/Contracts';
import ContractsList from '../components/ContractsList'; import ContractsList from '../components/ContractsList';
import {getContracts} from "../reducers/selectors"; import {getContracts} from "../reducers/selectors";
import PageHead from "../components/PageHead"; import PageHead from "../components/PageHead";
import Loading from '../components/Loading';
import Error from '../components/Error';
const MAX_CONTRACTS = 10; const MAX_CONTRACTS = 10;
@ -78,16 +80,25 @@ class ContractsContainer extends Component {
} }
render() { render() {
const {error, loading, mode, updatePageHeader} = this.props;
if (error) {
return <Error error={error} />;
}
if (loading) {
return <Loading />;
}
this.resetNums(); this.resetNums();
let ContractsComp; let ContractsComp;
if (this.props.mode === "detail") { if (mode === "detail") {
ContractsComp = Contracts ContractsComp = Contracts
} else if (this.props.mode === "list") { } else if (mode === "list") {
ContractsComp = ContractsList ContractsComp = ContractsList
} }
return ( return (
<React.Fragment> <React.Fragment>
{this.props.updatePageHeader && {updatePageHeader &&
<PageHead title="Contracts" <PageHead title="Contracts"
description="Summary of all deployed contracts" />} description="Summary of all deployed contracts" />}
<ContractsComp contracts={this.currentContracts} <ContractsComp contracts={this.currentContracts}
@ -115,7 +126,9 @@ ContractsContainer.propTypes = {
stopContracts: PropTypes.func, stopContracts: PropTypes.func,
fiddleContracts: PropTypes.array, fiddleContracts: PropTypes.array,
mode: PropTypes.string, mode: PropTypes.string,
updatePageHeader: PropTypes.bool updatePageHeader: PropTypes.bool,
error: PropTypes.string,
loading: PropTypes.bool
}; };
ContractsContainer.defaultProps = { ContractsContainer.defaultProps = {

View File

@ -9,7 +9,6 @@ import {
} from 'reactstrap'; } from 'reactstrap';
import { import {
contracts as contractsAction,
commands as commandsAction, commands as commandsAction,
commandSuggestions as commandSuggestionsAction, commandSuggestions as commandSuggestionsAction,
listenToProcessLogs, listenToProcessLogs,
@ -22,7 +21,7 @@ import Console from '../components/Console';
import {EMBARK_PROCESS_NAME, LOG_LIMIT} from '../constants'; import {EMBARK_PROCESS_NAME, LOG_LIMIT} from '../constants';
import PageHead from '../components/PageHead'; import PageHead from '../components/PageHead';
import ServicesContainer from './ServicesContainer'; import ServicesContainer from './ServicesContainer';
import {getContracts, getProcesses, getProcessLogs, getServices, getCommandSuggestions} from "../reducers/selectors"; import {getProcesses, getProcessLogs, getServices, getCommandSuggestions} from "../reducers/selectors";
import ContractsContainer from "./ContractsContainer"; import ContractsContainer from "./ContractsContainer";
class HomeContainer extends Component { class HomeContainer extends Component {
@ -45,7 +44,6 @@ class HomeContainer extends Component {
this.props.fetchProcessLogs(processName, LOG_LIMIT); this.props.fetchProcessLogs(processName, LOG_LIMIT);
this.props.listenToProcessLogs(processName); this.props.listenToProcessLogs(processName);
this.props.fetchContracts();
this.setState({activeProcess: processName}); this.setState({activeProcess: processName});
} }
@ -71,17 +69,14 @@ class HomeContainer extends Component {
</Card> </Card>
)} /> )} />
<DataWrapper shouldRender={this.props.contracts.length > 0} {...this.props} render={({contracts}) => (
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Deployed Contracts</CardTitle> <CardTitle>Deployed Contracts</CardTitle>
<div style={{marginBottom: '1.5rem', overflow: 'auto'}}> <div style={{marginBottom: '1.5rem', overflow: 'auto'}}>
<ContractsContainer contracts={contracts} mode="list" numContractsToDisplay={5} updatePageHeader={false} /> <ContractsContainer mode="list" numContractsToDisplay={5} updatePageHeader={false} />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
)} />
</React.Fragment> </React.Fragment>
); );
@ -97,16 +92,13 @@ HomeContainer.propTypes = {
stopProcessLogs: PropTypes.func, stopProcessLogs: PropTypes.func,
fetchProcessLogs: PropTypes.func, fetchProcessLogs: PropTypes.func,
listenToProcessLogs: PropTypes.func, listenToProcessLogs: PropTypes.func,
fetchContracts: PropTypes.func, services: PropTypes.array
services: PropTypes.array,
contracts: PropTypes.array
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
processes: getProcesses(state), processes: getProcesses(state),
services: getServices(state), services: getServices(state),
contracts: getContracts(state),
error: state.errorMessage, error: state.errorMessage,
processLogs: getProcessLogs(state), processLogs: getProcessLogs(state),
commandSuggestions: getCommandSuggestions(state), commandSuggestions: getCommandSuggestions(state),
@ -120,7 +112,6 @@ export default connect(
postCommand: commandsAction.post, postCommand: commandsAction.post,
postCommandSuggestions: commandSuggestionsAction.post, postCommandSuggestions: commandSuggestionsAction.post,
fetchProcessLogs: processLogsAction.request, fetchProcessLogs: processLogsAction.request,
fetchContracts: contractsAction.request,
listenToProcessLogs, listenToProcessLogs,
stopProcessLogs stopProcessLogs
} }

View File

@ -31,11 +31,15 @@ class ServicesContainer extends Component {
ServicesContainer.propTypes = { ServicesContainer.propTypes = {
fetchServices: PropTypes.func, fetchServices: PropTypes.func,
listenToServices: PropTypes.func, listenToServices: PropTypes.func,
error: PropTypes.string,
loading: PropTypes.bool
}; };
function mapStateToProps(state, _props) { function mapStateToProps(state, _props) {
return { return {
services: getServices(state) services: getServices(state),
error: state.errorMessage,
loading: state.loading
}; };
} }