I’ve been chasing this for years - source-control as a backing-store for live application configuration, AKA Feature Toggles. Not at all the same as source-control as the store for our development bits and pieces.

In 2012/13, with former colleague Logan McGrath I had a proof of concept app-config-app on Github, with implementations for Perforce, Git and Subversion (to varying degrees of completeness).

The excellent RhodeCode is now open source, and I thought I’d get busy with its Python and Pyramid templating to add a capability for custom editing controls. It turns out that I can do a scalable custom-editing system with an out the box RhodeCode, and some JavaScript fu.

Editing Technology

Inside RhodeCode, I have made a repo called editing.

Root Editor Script

Inside the editing repo, I have a source root-editor.js that recognizes that an edit is happening, and specifically one for ‘aardvark_configuration’ and inserts an editor specifically for it (taking it from an adjacent file in the editing repo - see below):

function swapEditor(outer, inner) {
    console.log("inner loaded");
    var payload = outer.replace('<!--EDITOR-HERE-->', inner);
    $(payload).insertBefore("#editor_container");
    console.log("load performed");
    // Hide CodeMirror's editor
    $("#editor_container").css("display", "none");
    // Hide a set of widgets that control CodeMirror
    $("div.form").css("display", "none");
}

$(document).ready(function () {
    var locn = window.location.href;
    if (locn.endsWith("#edit")) {
        $.get("/editing/raw/tip/outer_editor.html", function (outer) {
            console.log("outer loaded");
            // TODO more elegant regex and non-hard coded solution
            if (locn.indexOf("/aardvark_configuration.json") > 0) {
                $.get("/editing/raw/tip/aardvark_configuration.html", function (inner) {
                    swapEditor(outer, inner);
                });
            }
        });
    }
});

Note that I’m addressing the resource in the editing repo with /editing/raw/tip/. That way it’s always pulling in raw content at head revision. Find ‘tip’ in this page to see more of the same.

Framework code

Inside the editing repo, there’s a small HTML/JavaScript framework that knows how to be a RhodeCode editor:

<div>
    <script type="text/javascript">
        var app = angular.module("CustomEditor", []);

        // Needed to boot Angular into a page, after the browser has loaded the original page
        var html = document.querySelector('html');
        html.setAttribute('ng-app', '');
        html.setAttribute('ng-csp', '');

        app.controller('CustomEditorController', ['$scope', function ($scope) {

            // steal the JSON from RhodeCode's CodeMirror textarea
            $scope.cfg =  angular.fromJson($("#editor").val());

            $scope.addListItem = function (list, item) {
                list.push(item);
                $scope.change();
            };
            $scope.removeListItem = function (list, item) {
                var newList = [];
                while (list.length > 0) {
                    var n = list.pop();
                    if (n != item) {
                        newList.push(n);
                    }
                }
                while (newList.length > 0) {
                    list.push(newList.pop());
                }
                $scope.change();
            };
            $scope.change = function () {

                // aggressively put the JSON back into RhodeCode's CodeMirror textarea
                // as the "commit" button can be clicked at any time

                var textArea = document.getElementById('editor');
                var editor = CodeMirror.fromTextArea(textArea);
                editor.getDoc().setValue(angular.toJson($scope.cfg, 2));

            };
        }]);

        // Also needed to boot Angular into a page, after the browser has loaded the original page
        angular.bootstrap(document, ['CustomEditor'], []);

    </script>
    <div ng-app="CustomEditor" ng-controller="CustomEditorController" id="cfg_form">
        <style scoped>
            td {
                padding: 7px;
            }
        </style>
        <!--EDITOR-HERE-->
    </div>
</div>

Yes, root-editor.js really is looking for that <!--EDITOR-HERE--> string and replacing it with the html of an editor, in a rudimentary plug-in design.

This existed before in app-config-app here.

First Custom Editor

Also inside editing, I have a aardvark_configuration.html which fairly simple HTML, albeit with AngularJS attributes:

<table>
    <tr>
    <tr>
        <td align="right">
            <label for="lighton">Light is on:</label>
        </td>
        <td>
            <input data-ng-model="cfg.lighton" id="lighton" type="checkbox" data-ng-change="change()"/>
        </td>
    </tr>
    <tr>
        <td align="right">
            <label for="defaultErrorReceiver">Default Error Receiver (email):</label>
        </td>
        <td>
            <input data-ng-model="cfg.defaultErrorReceiver" id="defaultErrorReceiver"
                   required="required" type="email" size="35" data-ng-change="change()"/>
        </td>
    </tr>
    <tr>
        <td align="right">
            <label for="loadMaxPercent">Max Load Percentage:</label>
        </td>
        <td>
            <input data-ng-model="cfg.loadMaxPercent" id="loadMaxPercent" max="100"
                   min="0" required="required" size="3" maxlength="3"
                   style="width: 60px;" type="number" data-ng-change="change()"/>
        </td>
    </tr>
    <tr>
        <td align="right">
            <label for="nextShutdownDate">Next Shutdown Date:</label>
        </td>
        <td>
            <input data-ng-model="cfg.nextShutdownDate" data-ng-pattern="/\d\d?\/\d\d?\/\d\d\d\d/"
                   id="nextShutdownDate" required="required" type="text" data-ng-change="change()"/>
        </td>
    </tr>
    <tr>
        <td align="right" valign="top" style="font-weight: bold;">
            Banned nicknames:
        </td>
        <td>
            <ol style="padding-left: 12px">
                <li data-ng-repeat="nick in cfg.bannedNicks">
                          <span>
                            &nbsp;&nbsp;
                            <a data-ng-click="removeListItem(cfg.bannedNicks, nick)">[X]</a>
                          </span>
                </li>
            </ol>
            <form data-ng-submit="addListItem(cfg.bannedNicks, newNickname)">
                <input data-ng-model="newNickname" id="newNickname" size="20" type="text"/>
                <input type="submit" value="&#9754; Ban Nickname"/>
            </form>
        </td>
    </tr>
</table>

This existed before in app-config-app here.

RhodeCode settings changes

As administrator of my newly installed RhodeCode, I can setup the root-editor.js and Angular itself:

Using the thing

Here’s what an editing panel look like normally with the CodeMirror editor:

Here’s what it looks like with the custom editor:

And from app-config-app again, aardvark_configuration.json, which is the thing being edited by the custom editor:

{
  "bannedNicks": [
    "dino",
    "werwer",
    "derek",
    "jjjj",
    "ffff",
    "www",
    "wwwwwww"
  ],
  "defaultErrorReceiver": "piglet@thoughtworks.com",
  "lighton": true,
  "loadMaxPercent": 88,
  "nextShutdownDate": "8/9/2012"
}

RhodeCode shows a list of commits for the aardvark_configuration.json resource, and diffs look as tidy as you would hope for.

Where next?

This time last year, I did a proof of concept on Github, Custom JSON Editors for Github·com, but I can’t really go ahead with something that requires a browser plugin.

RhodeCode allows repository choices for Git, Mercurial and Subversion, with inline editing. So we get custom JSON editors for any of those three. Of course there’s YAML or the newer TOML that we might prefer for terser diffs (and easier merges, if needs be).

It could be that it be better to implement a pluggable custom editor design on the server side of RhodeCode, as I though it would do at the start of this mini-project. If they agree with the need, I hope the RhodeCode developers don’t make JavaScript choices that stymie my use of AngularJS as the editor technology, though. Either way, it needs to be more modular that I have it now. Logan made it very modular for the old app-config-app proof of concept, and the need for that has not gone away.

Also not done, is any of the tooling around the merging that is part of the ‘promotion’ aspect of a Config as Code system. More for another day, perhaps.

Updates

June 29th, 2016 - refactored HTML/JS. Also I’ll note now that the RhodeCode leads liked this thing.



Published

June 26th, 2016
Reads:

Tags

Categories