The App-Config-App now lets you promote changes between environments!
How does it work?
Perforce lets you create mappings to define the relationship between two diverging code branches. This allows for easy integration of changes between the two branches by referencing the name of the mapping.
If you login to App-Config-App and go to “Promote Changes,” you get an interface showing these relationships:
Changes between environments can be promoted in either direction along a mapping configuration. The receiving environment accepts all changes (developers would know this as an ‘accept-theirs’ resolution) and you are then allowed to review the changes by clicking on the “Pending Changes” link.
For example, I’ve promoted changes from “qa” to “dev”:
I can then review the changes by clicking on “Pending Changes”:
Changes may be edited or reverted before committing them.
Promoting changes using P4V
P4V is the Perforce visual client. Using P4V, you have much greater control over how changes get promoted, but it requires a little more work.
I’ve connected P4V to my App-Config-App user workspace to perform the same promotion from “qa” to “dev”:
Select the “qa” folder, then from the menu bar go to “Actions” > “Merge/Integrate”. This will bring up a wizard for performing the integration.
And ensure the direction of integration is “Target” < “Source”:
Finally, click “Merge”. If you expand the “dev” folder, you can see the where the changes are:
You are now free to modify the files further before finally committing the changes.
How it compares
You get greater options when using P4V to promote changes, but producing the same result as App-Config-App’s default behavior is fairly involved. If you aren’t paying attention or don’t know what you’re doing, you might break something :(
@TODO
More options for resolving changes
When you promote changes in App-Config-App, the source changes will overwrite the destination. This behavior reduces the chance for a conflict to happen, but it means you really have to pay attention to what’s changed in the destination config and possibly edit the config further before finally committing it.
Conflict resolution
If a conflict occurs after promoting changes, a screen should be available for viewing and editing the conflicting changes.
Better error reporting if promotion fails due to permissions
Users with read-only access to multiple environments will still be able to promote changes between them. The promotion doesn’t actually occur (the files remain unchanged) but the application doesn’t report any errors when this happens.
Paul Hammant found this cool Server-Side Piano and I’ve modified it to be configurable from a running App-Config-App. Because the sound is generated at the server, you’re able to see (hear) the Server-Side Piano change its configuration without reloading the UI.
Making it work for yourself
I’ve updated the App-Config-App with additional configuration to support choosing which instrument the Server-Side Piano will play. A clean install of App-Config-App using setup_examples.rb will provide everything needed to run the Server-Side Piano.
The application’s configuration URL and credentials are located in web.xml. Additional details may be found in the application’s README.
Continuing from my last post, I’ve forked Paul Hammant’s original App-Config-App and modified it to work against Perforce. I’ve decided not to continue using Perforce Chronicle as it is primarily intended for content management.
With this version, App-Config-App is written in Ruby, mostly using Sinatra, a lightweight web application framework. I’m still using AngularJS, but I’ve also added a few other things:
A .rvmrc file, so you automagically switch to Ruby 1.9.3
A Gemfile, so you don’t have to install everything individually :)
You’ll notice I made the extra effort to add colors and drop shadows :D The application works from the project root in Perforce, so the files in each branch are viewable here. Clicking on “Dev” > “aardvark_configuration.html” will bring up a form for editing aardvark_configuration.json as in the previous version:
Changes to the form data are automatically saved. After making a view edits, you can click “View Diff” to get the diffs or “Revert” your changes. Go ahead and change the email address and fiddle around with the banned nicks, then go click “Pending Changes”:
This screen shows all files that were changed and their diffs as well. You can “Revert” each file individually, and if you want to commit all changes, then enter a commit message and click “Commit Changes”. If you commit the changes and go back to “Dev” > “aardvark_configuration.html”, you’ll see the new values in the form:
Security and Permissions
Permissions and security are managed through Perforce. For users to be able to login, they must have a user and client configured in Perforce. Those users must also have permissions configured in order to view or modify files.
The setup_example.rb script creates three test users to demonstrate branch permissions:
12345
Username Password Write Read
-------------------------------------------------
sally-runtime bananas prod staging, dev
jimmy-qa apples staging dev
joe-developer oranges dev
Logging in as any of these users will hide branches that don’t have at least read-level access, and branches that don’t have write-level access won’t allow changes.
All users created by setup_example.rb are intended only as examples. In the real world, all application users should be setup with real logins and real permissions.
It is this support for users and per-branch permissions that I am using Perforce as the SCM backend rather than Git.
Application Users
The setup_example.rb script also sets up three application users to demonstrate how an application would consume configuration:
Application user accounts are configured in Perforce like any other user. I highly recommend that application users be given ready-only access to individual files rather than entire branches.
Divergence
Right now, App-Config-App offers no UI tools for managing divergence and merging. Merges must be performed outside App-Config-App, and the specific safety nets to prevent nefarious change vulnerabilities are dependent on your branch specs and permissions configuration.
There are also are no tools to manage conflicts of existing edits with incoming changes from another user. If a Perforce sync fails due to a conflict, you are best to revert all changes and enter them again.
@TODO
A better model for autosave
Autosave in AngularJS isn’t very good. AngularJS doesn’t integrate with DOM events the way idiomatic JavaScript does, or provide a reasonable abstraction the way Dojo Toolkit or JQuery do. Right now, autosave in App-Config-App triggers with every key press in the config forms, and pummels the back-end server with ajax posts.
I’ve also noticed that the autosave triggers even when a value is invalid. The first time an email address, for example, becomes invalid, AngularJS will post back the JSON, but without the invalid email address field–the invalid field is entirely left out of the JSON structure. After that, AngularJS will stop autosaving until the value is valid. There are also no measures in place to prevent a user from leaving an invalid value and saving an incomplete JSON file.
A better model for validation
AngularJS does not offer a good validation API. The validation API is quite opaque and I haven’t found any real examples using it. The built-in form validation is inadequate. There are few ng-* HTML attributes exposing more than basic configuration parameters, and no hooks offered as extension points.
For example, I’m using regular expressions for date validation in App-Config-App. There isn’t a hook to provide custom validation checks, and regular expressions don’t perform sanity checks. Values such as “00/00/0000” will pass validation.
More example clients than the Java one needed
The App-Config-Java client is enough to show the basic idea behind caching and reloading configuration from App-Config-App. I would like to create a few more examples in a couple different platforms, possibly also showcasing “hot reconfiguration” for feature toggles.
Someone should port this to Subversion or TFS
App-Config-App should be usable by the largest possible audience. For instance, if you’re using Subversion, then you should be able to take advantage of the existing infrastructure.
The reason I point out Subversion and TFS is largely due to support of per-branch permissions.
Perforce is an enterprise-class source control management (SCM) system, remarkably similar to Subversion (Subversion was inspired by Perforce :) Perforce is more bulletproof than Subversion in many ways and it’s generally faster. Git does not impose any security constraints or permissions on branches, Perforce gives comprehensive security options allowing you to control access to different branches: for example, development, staging, and production. Subversion, however, can support permissions on branches with some extra configuration (Apache plus mod_dav_svn/mod_dav_authz). For these reasons, Perforce is a better option for storing configuration data than either Git or Subversion.
Perforce CMS as an application server
Perforce Chronicle is a content management system (CMS) using Perforce as the back-end store for configuration and content. The app-config application is built on top of Chronicle because Perforce does not offer a web view into the depot the way Subversion can through Apache. Branching and maintaining divergence between environments can be managed through the user interface, and Chronicle provides user authentication and management, so access between different configuration files can be restricted appropriately. The INSTALL.txt file that is distributed with Chronicle helps with an easy install, mine being set up to run locally from http://localhost.
There is a key issue in using Chronicle, however. The system is designed for the management of content and not necessarily arbitrary files. In order to make the app-config application work, I had to add a custom content type and write a module. Configuration and HTML are both plain-text content, so I created a “Plain Text” content type with the fields title and content:
Go to “Manage” > “Content Types”
Click “Add Content Type”
Enter the following information:
123456789101112131415161718
Id: plaintext
Label: Plain Text
Group: Assets
Elements:
[title]
type = text
options.label = Title
options.required = true
display.tagName = h1
display.filters.0 = HtmlSpecialChars
[content]
type = textarea
options.label = Content
options.required = true
display.tagName = pre
display.filters.0 = HtmlSpecialChars
Click “Save”.
The Config App
I’ve borrowed heavily from Paul’s app-config HTML page, which uses AngularJS to manage the UI and interaction with the server. Where Paul’s app-config app used the jshon command to encode and decode JSON, Zend Framework has a utility class for encoding, decoding, and pretty-printing JSON, and Chronicle also ships with the simplediff utility for performing diffs with PHP.
The source JSON configuration is the same, albeit sorted:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><htmllang="en"xmlns:ng="http://angularjs.org"><head><metahttp-equiv="content-type"content="text/html; charset=UTF-8"><title>Configuration application (alpha)</title><script type="text/javascript"ng:autobindsrc="http://code.angularjs.org/0.9.19/angular-0.9.19.min.js"></script><style type="text/css">ins{color:#00CC00;text-decoration:none;}del{color:#CC0000;text-decoration:none;}</style></head><bodyng:controller="AppCfg"><script type="text/javascript">functionAppCfg($resource,$xhr){varself=this;this.newNickname="";this.svrMessage;this.message;this.cfg=$resource("/appconfig/stack_configuration.json").get({});this.save=function(){self.cfg.$save({message:self.message},function(){alert("Config saved to server");},function(){alert("ERROR on save");});self.message="";};this.newNick=function(){self.cfg.bannedNicks.push(self.newNickname);self.newNickname="";};this.diffs=function(){$xhr("post","/appconfig/diffs/stack_configuration.json",angular.toJson(self.cfg),function(code,svrMessage){self.svrMessage=svrMessage;});};this.deleteNick=function(nick){varoldBannedNicks=self.cfg.bannedNicks;self.cfg.bannedNicks=[];angular.forEach(oldBannedNicks,function(n){if(nick!=n){self.cfg.bannedNicks.push(n);}});};}AppCfg.$inject=["$resource","$xhr"];</script> Light is on: <inputtype="checkbox"name="cfg.lighton"/><br/> Default Error Reciever (email): <inputname="cfg.defaultErrorReciever"ng:validate="email"/><br/> Max Load Percentage: <inputname="cfg.loadMaxPercent"ng:validate="number:0:100"/><br/> Next Shutdown Date: <inputname="cfg.nextShutdownDate"ng:validate="date"/><br/> Banned nicks:
<ol><ling:repeat="nick in cfg.bannedNicks"><span>{{nick}} <ang:click="deleteNick(nick)">[X]</a></span></li></ol><formng:submit="newNick()"><inputtype="text"name="newNickname"size="20"/><inputtype="submit"value="<-- Add Nick"/><br/></form><hr/><buttonng:click="diffs()">View Diffs</button><br/><buttonng:disabled="{{!message}}"ng:click="save()">Commit Changes</button> Commit Message: <inputname="message"></input><br/> Last Server operation: <br/><divng:bind="svrMessage | html:'unsafe'"></div></body></html>
Both of these assets were added by performing:
Click “Add” from the top navbar
Click “Add Content”
Select “Assets” > “Plain Text”
For “Title”, enter “index.html” or “stack_configuration.json”
Paste in the appropriate “Content”
Click “URL”, select “Custom”, and enter the same value as “Title” (otherwise, Chronicle will convert underscores to dashes, so be careful!)
Click “Save”, enter a commit message, then click the next “Save”
Both assets should be viewable as mangled Chronicle content entries from http://localhost/index.html and http://localhost/stack_configuration.json. You normally will not use these URLs.
At this point, neither asset is actually usable. Most content is heavily decorated with additional HTML and then displayed within a layout template, but I want both the index.html and stack_configuration.json assets to be viewable as standalone files and provide a REST interface for AngularJS to work against.
Come back PHP! All is forgiven
Chronicle is largely built using Zend Framework and makes adding extra modules to the system pretty easy. My module needs to be able to display plaintext assets, update their content using an HTTP POST, and provide diffs between the last commit and the current content.
To create the module, the following paths need to be added:
INSTALL/application/appconfig
INSTALL/application/appconfig/controllers
INSTALL/application/appconfig/views/scripts/index
Declare the module with INSTALL/application/appconfig/module.ini:
<?phpdefined('LIBRARY_PATH')ordefine('LIBRARY_PATH',dirname(__DIR__));require_onceLIBRARY_PATH.'/simplediff/simplediff.php';classAppconfig_IndexControllerextendsZend_Controller_Action{private$entry;private$mimeTypes=array('.html'=>'text/html','.json'=>'application/json',);publicfunctionpreDispatch(){$request=$this->getRequest();$request->setParams(Url_Model_Url::fetch($request->getParam('resource'))->getParams());$this->entry=P4Cms_Content::fetch($request->getParam('id'),array('includeDeleted'=>true));}publicfunctionindexAction(){$this->getResponse()->setHeader('Content-Type',$this->getMimeType(),true);$this->view->entry=$this->entry;if($this->getRequest()->isPost()){$this->entry->setValue('content',$this->getJsonPost());$this->entry->save($this->getRequest()->getParam('message'));}}privatefunctiongetMimeType(){$url=$this->entry->getValue('url');$suffix=substr($url['path'],strrpos($url['path'],'.'));if(array_key_exists($suffix,$this->mimeTypes)){return$this->mimeTypes[$suffix];}else{return'text/plain';}}publicfunctiondiffsAction(){$this->getResponse()->setHeader('Content-Type','text/html',true);$this->view->diffs=htmlDiff($this->entry->getValue('content'),$this->getJsonPost());}publicfunctionpostDispatch(){$this->getHelper('layout')->disableLayout();}privatefunctiongetJsonPost(){if($this->getRequest()->isPost()){return$this->prettyPrint(file_get_contents('php://input'));}else{thrownewException('Can\'t get JSON without POST');}}privatefunctionprettyPrint($json){$array=Zend_Json::decode($json);$this->sort($array);returnZend_Json::prettyPrint(Zend_Json::encode($array),array('indent'=>' '));}privatefunctionsort(array&$array){if(count(array_filter(array_keys($array),'is_string'))>0){ksort($array);}foreach($arrayas&$value){if(is_array($value)){$this->sort($value);}}}}
AngularJS
After all files are in place, Chronicle needs to be notified that the new module exists by going to “Manage” > “Modules”, where the “Appconfig” module will be listed if all goes well :) Both assets will now be viewable from http://localhost/appconfig/index.html and http://localhost/appconfig/stack_configuration.json. AngularJS’ $resource service is used in index.html to fetch stack_configuration.json and post changes back.
From http://localhost/appconfig/index.html, the data from stack_configuration.json is loaded into the form:
Edits to stack_configuration.json can be made using the form, and the diffs viewed by clicking on “View Diffs”:
The changes can be saved by entering a commit message and clicking “Commit Changes”. After which, clicking “View Diffs” will show no changes:
To show that edits have in fact been made to stack_configuration.json, go to http://localhost/stack_configuration.json, select “History” and click on “History List”:
Chronicle also provides an interface for viewing diffs between revisions:
Disk Usage
Something to remember in using Chronicle is that each resource requested from Perforce is written to disk before being served to the client. This means that for each request to index.html, Chronicle allocates a new Perforce workspace, checks out the associated file, serves it to the client, then deletes the file and the workspace at the end of the request. This allocate/checkout/serve/delete cycle executes for stack_configuration.json and every other resource in the system.
@TODO
Security!
There’s one major flaw with the appconfig module: it performs zero access checks. By default, Chronicle can be configured to disallow anonymous access by going to “Manage” > “Permissions” and deselecting all permissions for “anonymous” and “members”. Logging out and attempting to access either http://localhost/appconfig/stack_configuration.json or http://localhost/appconfig/index.html will now give an error page and prompt you to log in. Clicking “New User” will also give an error, as anonymous users don’t have the permission to create users.
Access rights on content are checked by the content module, but are also hard-coded in the associated controllers as IF-statements. A better solution will be required for proper access management in the appconfig module.
Better integration
Chronicle’s content module provides JSON integration for most of its actions, but these mostly exist to support the Dojo Toolkit-enabled front-end. Integrating with these actions over JSON requires detailed knowledge of Chronicle’s form structures.
Chronicle has some nice interfaces for viewing diffs. If I could call those up from index.html I would be major happy :)
Automatic creation of plaintext content type
Before the appconfig module is usable, the plaintext content type has to be created. I would like to automate creation of the plaintext content type when the module is first enabled.
Making applications aware of updates to configuration
When stack_configuration.json is updated, there’s no way to notify applications to the change, and no interface provided so they may poll for changes. I’m not entirely sure at this point what an appropriate solution would look like. In order to complete the concept, I’d first have to create a client app dependent on that configuration.
Better interfaces for manipulating plaintext assets
I had to fiddle with index.html quite a bit. This basically involved editing a local copy of index.html, then pasting the entire contents into the associated form in Chronicle. I have not tried checking out index.html directly from Perforce, and I imagine that any edits would need to be made within Chronicle. Github offers an in-browser raw editor, and something like that would be real handy in Chronicle.
Handling conflicts
There is no logic in the appconfig module to catch conflicts if there are two users editing the same file. Conflicts are detectible because an exception is thrown if there is a conflict, but I’m not sure what the workflow for resolution is in Chronicle terms, or how to integrate with it. Who wins?
Working with branches
I did not take the time to see how Chronicle manages branches. I will need to verify that Chronicle and the appconfig module can work with development, staging, and production branches, with maintained divergence. For example, we’re still trying to figure out how to attach visual clients like P4V to the repository and work independently of Chronicle.
Kudos
I would like to thank the guys at Perforce for their assistance and answering all my questions as I worked with Chronicle, especially Randy Defauw.