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 api!: Server;
private apiUrl!: string;
private isServiceRegistered = false;
constructor(private embark: Embark, private options: any) {
this.embark.events.emit("status", __("Starting API & Cockpit UI"));
@ -22,7 +23,11 @@ export default class Api {
this.listenToCommands();
this.registerConsoleCommands();
this.init();
});
}
private init() {
this.embark.events.request("processes:register", "api", {
launchFn: (cb: (error: Error | null, message: string) => void) => {
this.api.start()
@ -44,27 +49,39 @@ export default class Api {
}
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() {
if (this.isServiceRegistered) {
return;
}
this.isServiceRegistered = true;
this.embark.events.request("services:register", "api", (cb: (options: object) => any) => {
checkIsAvailable(this.apiUrl, (isAvailable: boolean) => {
const devServer = __("Cockpit UI") + " (" + this.apiUrl + ")";
const serverStatus = (isAvailable ? "on" : "off");
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() {
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:stop", (cb) => this.embark.events.request("processes:stop", "api", cb));
this.embark.events.setCommandHandler("api:start", (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.api.enableLogging();
cb();
@ -79,16 +96,24 @@ export default class Api {
this.embark.registerConsoleCommand({
description: __("Start or stop the API"),
matches: ["api start"],
process: (cmd: string, callback: () => void) => {
this.embark.events.request("api:start", callback);
process: (cmd: string, callback: (msg: string) => void) => {
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",
});
this.embark.registerConsoleCommand({
matches: ["api stop"],
process: (cmd: string, callback: () => void) => {
this.embark.events.request("api:stop", callback);
process: (cmd: string, callback: (msg: string) => void) => {
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 helmet from "helmet";
import * as http from "http";
import * as net from "net";
import * as path from "path";
import * as ws from "ws";
@ -28,6 +29,8 @@ export default class Server {
private isLogging: boolean = false;
private server?: http.Server;
private openSockets = new Set<net.Socket>();
constructor(private embark: Embark, private port: number, private hostname: string, private plugins: Plugins) {
this.expressInstance = this.initApp();
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, () => {
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));
}
// close any open sockets
this.openSockets.forEach((socket) => socket.destroy());
this.server.close(() => {
this.server = undefined;
resolve();
@ -198,17 +213,17 @@ export default class Server {
instance.app.use(cors());
instance.app.use(bodyParser.json());
instance.app.use(bodyParser.urlencoded({extended: true}));
instance.app.use(bodyParser.urlencoded({ extended: true }));
instance.app.ws("/logs", (websocket: ws, _req: Request) => {
this.embark.events.on("log", (level: string, message: string) => {
websocket.send(JSON.stringify({msg: message, msg_clear: message.stripColors, logLevel: level}), () => {});
websocket.send(JSON.stringify({ msg: message, msg_clear: message.stripColors, logLevel: level }), () => { });
});
});
if (this.plugins) {
instance.app.get("/embark-api/plugins", (_req, res: Response) => {
res.send(JSON.stringify(this.plugins.plugins.map((plugin) => ({name: plugin.name}))));
res.send(JSON.stringify(this.plugins.plugins.map((plugin) => ({ name: plugin.name }))));
});
const callDescriptions: CallDescription[] = this.plugins.getPluginsProperty("apiCalls", "apiCalls");

View File

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

View File

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

View File

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