Login

Simple File Based Application Configuration

I thought this was too simple to bother posting about, but someone requested it, so here it is. Having made my journey to Linux about a year ago, I've found a new appreciation for the simplicity of plain old text files and the file system in general.

Some of the tools I've come to rely on like Daemontools from Daniel Bernstein also show this appreciation of the Unix way, small simple reliable tools that just work and are configured easily with plain text files. The most recent versions of Apache have also broken up the monolithic httpd.config into much smaller simpler individual files spread out over various directories that greatly ease automation by keeping individual site settings in separate files and using simple symbolic links to enable or disable individual sites.

Inspired by the simplicity and ease of managing these tools, I decided my Smalltalk programs, while ungodly easy to write, were much too hard to configure and deploy because configuration was based either on code in the image or in the case of Seaside, its custom configuration system, neither of which are simple or easy to move around between images and servers. I also like how Ruby on Rails configuration system allows multiple configurations that you can easily switch between to run your code in your various environments.

Rather than trying to design some ultimate configuration system, I just looked at how I write and deploy my applications, which is always one image per application in its own directory (possibly launched multiple times on various ports) and decided the simplest thing that could possibly work for me was just a directory named config, in my image directory, with three subdirectories named prod, dev, and test, and a single file named config which tells me which config subdirectory is active.

So I'll declare a configuration class...

Object subclass: #SSConfig
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SentorsaSeaside-Configuration'

and a class side #create method to setup this structure on the filesystem...

create
    | configDir |
    configDir := (FileDirectory default directoryNamed: #config) assureExistence.
    FileStream 
        forceNewFileNamed: (configDir fullNameFor: #config)
        do: [ : f | f nextPutAll: 'dev' ].
    (configDir directoryNamed: #prod) assureExistence.
    (configDir directoryNamed: #dev) assureExistence.
    (configDir directoryNamed: #test) assureExistence

Next, I want a simple dictionary style API, "SSConfig at: #someSetting", so I'll need the core class side method #at: which grabs the current configuration and uses the key you hand it to looks for the file name that matches, opening it in read only mode if it exists or simply creating it if it doesn't.

at: aKey 
    | currentConfig configDir value |
    currentConfig := FileStream 
        readOnlyFileNamed: ((FileDirectory default directoryNamed: #config) fullNameFor: #config)
        do: [ : f | f contentsOfEntireFile ].
    configDir := (FileDirectory default directoryNamed: #config) directoryNamed: currentConfig.
    value := (configDir fileExists: aKey) 
        ifTrue: 
            [ FileStream 
                readOnlyFileNamed: (configDir fullNameFor: aKey)
                do: [ : f | f contentsOfEntireFile ] ]
        ifFalse: 
            [ FileStream 
                forceNewFileNamed: (configDir fullNameFor: aKey)
                do: [ : f | f contentsOfEntireFile ] ].
    ^ (value endsWith: Character lf asString) 
        ifTrue: [ value allButLast ]
        ifFalse: [ value ]

Next I want to be able to ask for a value and provide a default in case there is no configured value, built simply upon the previous method...

at: aKey default: aValue 
    ^(self at: aKey) 
        ifEmpty: [ aValue ]
        ifNotEmptyDo: [ : it | it ]

I also want to be able to ask for a config value typed to a boolean (SSConfig can: #useBlaBla) for the common case of true/false settings, another simple method built upon #at:...

can: aKey
    ^(self at: aKey) = 'true'

And of course, having strings and booleans, I now want a couple of simple methods for asking for integers from config...

numAt: aKey 
    ^(self at: aKey) 
        ifEmpty: [ 0 ]
        ifNotEmptyDo: [ : it | it asInteger ]

numAt: aKey default: aValue
    ^(self at: aKey) 
        ifEmpty: [ aValue ]
        ifNotEmptyDo: [ : it | it asInteger ]

These are all class side methods, and so far I've not found the need for anything more than strings, bools, and integers. If I do, I'll extend it more, however, this seems to be all I need for all of my configuration needs thus far. It's simple, every setting is its own file, and it allows me to easily switch between various configs and easily share configurations between various images with nothing more than a simple file copy. I can also easily edit the configuration from the shell, where I do most of my administration tasks.

NOTE: This method is not application aware, so it won't work well if you run multiple applications within a single image, but I'm not writing code for use cases I don't have and it works well for how I deploy my applications.

Comments (automatically disabled after 1 year)

miguel 5859 days ago

Good article,

Nevertheless, why writing a dedicated file for each setting? Why not writing one simple file with all of the application settings? In Unix filesystems (not with reiserfs), the number of available inodes are limited and each new file consume an inode.

Yes, of course, it will be more expensive to parse a such file to get just one setting, but we can improve that by writing a parser that does its work in a background at regular time and that populates a dictionary from the parsed settings.

Ramon Leon 5859 days ago

I think you missed the point about simplicity, or at least how serious I am about keeping things stupidly simple. If I have to start writing a parser, it isn't simple anymore. I usually find I have maybe 10 or 12 config settings for a typical application. If I'm worried about creating 12 files, something else is seriously wrong with me.

Sebastian 5857 days ago

Hi Ramon, I do something similar. I keep appart dev, test and prod in its own directories (that way they has its own odb also).

I'm curious, your qa profile is same use as the testing stage? also XHTML validation? other?

cheers !

Ramon Leon 5857 days ago

Yea, qa is short for quality assurance, means same thing as test. Not sure what you mean about XHTML validation. Dev is local machine, Qa is test environment on production machines, and of course Prod is just Production.

Norbert Hartl 5828 days ago

Nice and simple. I somehow like the MetaConfig config/config file :) But why don't you use a startup script for the squeak image? It is simple, too.

I want to configure seaside as well. So using a startup script and

configuration := (
   (WADispatcher default)
      entryPointAt: 'mywebapp'
) configuration.

configuration valueAt: #database put: 'dbname'.

The rest of my application config goes into a configuration object which is also populated from the startup script

Ramon Leon 5828 days ago

Because I can, and often do, change my config files on the fly and want them to take effect immediately. Feeding the image on startup isn't really an option. If I add an IP to an authorized IP list for example, that needs to happen without restarting the image.

about me|good books|popular posts|atom|rss