saltstack/salt

View on GitHub
salt/netapi/rest_cherrypy/index.html

Summary

Maintainability
Test Coverage
<!doctype html>
<html>
<script>
(function() {
    // -- Scaffolding ---------------------------------------------------------
    // Functions and observables that make up the application.

    // Store the session token in-memory. Real apps will want to use
    // localstorage and proper escaping to guard against XSS.
    var XAuthToken;

    var Observable = getObservable();

    // Various Salt event tag matchers.
    var isJobEvent = fnmatch('salt/job/*');
    var isJobNew = fnmatch('salt/job/*/new');
    var isJobReturn = fnmatch('salt/job/*/ret/*');

    // An observable stream of Salt events.
    var saltEvents = new Observable(function(observer) {
        var source = new EventSource('/events?salt_token=' + XAuthToken);

        source.onmessage = function(message) {
            observer(JSON.parse(message.data));
        };
    });

    // Keep a counter of all events seen.
    var eventCounter = saltEvents.scan(count => count += 1, 0);

    // Keep a list of recent job returns.
    // Salt's event stream is very busy and can easily overwhelm the available
    // memory for a browser tab so keep uneeded data out of the DOM.
    var jobReturnBufferSize = 10;
    var jobReturns = saltEvents.scan(function eventCounter(acc, ev) {
        var tag = ev.tag,
            data = ev.data;

        if (isJobReturn(tag)) {
            if (acc.length >= jobReturnBufferSize) {
                acc.pop();
            }
            acc.unshift(data);
        }

        return acc;
    }, [])
    // Transform the job returns into HTML.
    .map(function(returns) {
        var li = returns.map(function(ret) {
            return `
                <li title="${JSON.stringify(ret, null, 4)}">
                    ${ret.jid} : ${JSON.stringify(ret.return)}
                </li>
            `;
        });

        return `<ul>${li.join('')}</ul>`;
    });

    // Document ready.
    function ready(fn) {
        if (document.readyState != 'loading'){ fn(); }
        else { document.addEventListener('DOMContentLoaded', fn); }
    }

    // Handle the login form and store the resulting token.
    function login(callback) {
        return function(ev) {
            ev.preventDefault();

            post('/token', getFormData(ev, {}))
                .then(resp => resp.ok ? resp.json() : resp)
                .then(body => XAuthToken = body[0].token)
                .then(callback);
        };
    }

    // Handle the run job form.
    function runJob(ev) {
        ev.preventDefault();

        // The already-established SSE connection is very fast so we are likely
        // to get the job returns for quick jobs (`cmd.run`, `test.ping`, etc)
        // **before** this xhr call finishes to give us the resulting Salt
        // JID. We have a few options:
        //
        // 1. We simply output all job returns as they come in and don't worry
        //    about individual JIDs. This is the approach taken in this app
        //    which is why we're ignoring the xhr return below.
        // 2. We keep a buffer of JIDs run from this app, once they're
        //    available, and we highlight matching returns in the stream.
        // 3. We generate the JID here on the client and send it as a the jid
        //    kwarg to Salt so that we already have it for when the return
        //    rolls in.
        var data = getFormData(ev, {client: 'local_async', arg: []});
        post('/run', [data]);
    }

    // -- App -----------------------------------------------------------------
    // Activate and use all the scaffolding above at the right time.

    ready(function app() {
        var body = document.querySelector('body');

        if (!XAuthToken) {
            // Show the login form.
            var tmplLogin = document.querySelector('#tmpl-login');
            body.innerHTML = tmplLogin.innerHTML;

            document.querySelector('#login').onsubmit = login(app);
        } else {
            // Show the main app.
            var tmplEvents = document.querySelector('#tmpl-events');
            body.innerHTML = tmplEvents.innerHTML;

            document.querySelector('#runJob').onsubmit = runJob;

            var eventCounterDOM = document.querySelector('#eventCount');
            eventCounter.subscribe(content =>
                eventCounterDOM.textContent = content);

            var jobReturnsDOM = document.querySelector('#jobReturns');
            jobReturns.subscribe(content =>
                jobReturnsDOM.innerHTML = content);
        }
    });

    // -- Util ----------------------------------------------------------------
    // Misc utility functions.

    // A simple function to allow Salt-style globbing on event tags.
    function fnmatch(pattern) {
        if (pattern.indexOf('*') === -1) {
            return filename => pattern === filename;
        } else {
            // Taken from Lodash (MIT).
            // Use _.escapeRegExp directly if available.
            var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
            var escaped = pattern.replace(reRegExpChar, '\\$&');
            var matcher = new RegExp('^'+ escaped.replace(/\\\*/g, '.*') +'$');
            return filename => matcher.test(filename);
        }
    }

    // A wrapper around fetch to send/receive JSON and add the auth token.
    function post(path, body) {
        var headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        };
        if (XAuthToken) {
            body.forEach(x => x['token'] = XAuthToken);
        }
        return fetch(path, {
            method: 'post',
            body: JSON.stringify(body),
            headers: headers,
        });
    }

    // Get an object of form name/values.
    function getFormData(ev, seed) {
        return Array.prototype.reduce.call(ev.target, function(acc, el) {
            if (el.name && el.value !== '') {
                if (Array.isArray(acc[el.name])) {
                    acc[el.name].push(el.value);
                } else {
                    acc[el.name] = el.value;
                }
            }
            return acc;
        }, seed);
    }

    // A bare-bones Observable implementation without
    // cancelation or unsubscription or anything else.
    // Use a full implementation or an event emitter in real apps.
    function getObservable() {
        class Observable {
            constructor(_sub) { this._sub = _sub }
            subscribe(next) { this._sub(next) }
            map(f) { return new Observable(next =>
                this.subscribe(x => next(f(x)))) }
            do(f) { return new Observable(next =>
                this.subscribe(x => { f(x); next(x) }) )}
            scan(f, seed) { return new Observable(next => {
                var acc = seed;
                return this.subscribe(cur => { acc = f(acc, cur); next(acc) });
            }) }
            filter(f) { return new Observable(next =>
                this.subscribe(x => f(x) && next(x))) }
        }
        return Observable;
    }
})();
</script>

<script type='text/tmpl' id='tmpl-events'>
    <form id="runJob">
        <label>Target (compound): <input name="tgt"></label><br>
        <label>Function: <input name="fun"></label><br>
        <label>Arg: <input name="arg"></label><br>
        <label>Arg: <input name="arg"></label><br>
        <label>Arg: <input name="arg"></label><br>
        <button type="submit">Submit job</button>
    </form>

    <div>
        <p>Salt Events Seen: <span id="eventCount">0</span></p>
        <p>Salt Job Returns:</p>
        <ul id="jobReturns"></ul>
    </div>
</script>

<script type='text/tmpl' id='tmpl-login'>
    <form id="login">
        <label>Username: <input name="username"></label><br>
        <label>Password: <input name="password" type="password"></label><br>
        <label>Eauth: <input name="eauth"></label><br>
        <button type="submit">Login</button>
    </form>
</script>
</html>