Configs suck? Try a real programming language
beepb00p.xyz
beepb00p.xyz
Having a programmable config is how you end up with horrible interdependencies where function f() does foo, and you can't figure out why it's not blowing up, until you realize the only caller is dynamically configured in staging_env_4.json which is emitted by config_generator.py which always ensures the precondition for foo, but only if it's also generating prod_env.json at the same time, but it's OK because we always do that anyway, and you're not sure if you need another cup of coffee or whiskey.
For that reason, something looking more like a programing language feels more appropriate to me.
I kind of like the Jenkinsfile syntax for this use case as a CI workflow is not a static definition defined by a configuration file (yaml, json or ini) but the syntax is also not a fully "open" programing language like Python which would authorize too many potential hacks.
A specialized DSL in this case seems more appropriate and being the correct balance between flexibility and complexity, at least for most needs.
If you need a fully programmable environment, then you need a fully programmable environment. The important thing is that the DSL is a well-defined contract between what you specify and what the underlying software does, so if you say "If X then do Y", then that's exactly what it does. You should be able to follow the DSL line-by-line and debug it.
(You've probably noticed that I'm not a fan of declarative languages a la Makefile: you can never figure out where things "start" and "end".)
A "full language config" (like Python) is much worse than DSL: usually there's no clear semantics about when any snippet is executed, yet it's powerful enough to bring in the whole outside environment, which is what's going to happen in a team with 5+ people.
if i follow your logic i should replace the 5 if debug statements in my config.js with 50 littered throughout my codebase. Also i lose strong typing and in line documentation.
edit:ill add im against emitting json from a config script, that seems like additional complexity. just reference a file, you can use json if and only if you need program interop and reference it from the config script
With scripting languages theres no reason to use json for configurtion unless the config is shared between programs. The main reason config files exist is so you can edit the program without recompiling, i feel like people forgot that and kept blindly using json when js or ts is better. My config file is just like 20 lines but putting the extremely simple conditional logic of if its debug if its client if its server there reduces program complexity by a couple hundred sloc.
This isn't generally the reason. The main reason config files exist is so you can run the program in in a different state easily. State differs across environments, code should not. This is why you have a config file for prod and test and dev, but you shouldn't have different code for prod and test and dev. You still have to rebuild the app for those environments. See: https://12factor.net/config.
My ide lets me f12 to my config.js file and i can use editor hotkeys like comment toggle so by this metric my method is superior.
>You still have to rebuild the app for those environments.
As i said above you dont have to rebuild for python and javascript since they can load the config file on the fly. I use xml or json for compiled languages because beign able to change the program without recompiling is of critical importance.
>you shouldn't have different code for prod and test and dev.
You dont have different file for test/dev/prod, you have 1 config file that has a few inline if statments based off of runtime args. For me 1 file is less confusing than 2 or 3.
That sounds specific enough to be based on a very bad day or week at the office. You have my sympathies.
To be fair, I didn't do it... yet. I have family, I need salary. -_-
Another project team leader from a company they acquired regularly paid me in bottles of whiskey when I helped his team hit integration milestones for their product, as the main dev teams weren't helping him.
Here we're discussing destroying a data format and replacing it with a programming language because we can't be bothered to do static analysis on a data format?
LSP should have made this the golden era of static analysis, but instead everyone conflates type systems and static analysis so the industry gets myopic on typescript and friends
If you took static analysis out of typescript I wouldn't even give typescript a second look, meanwhile something like this where a given key should be one of a set we don't even consider how to do that without a type system
A data format doesn't have a type system and that's partly why they're great, type systems don't work across different programming languages but data does, data is a shared composable language
Data as a concept is more important than a type system and static analysis is more important than a type system
Static analysis is not magic, they need some more inputs other than your code. I can't say for sure but Rust's success told me at least some people are happy to write annotations specifically targeted at static analyzer (Lifetime etc).
You can do all these things for YAML or JSON or TOML or XML or INI or s-expressions or custom bytecode or butterflies or however else you're defining configuration data, too. It might not be present as part of the configuration language itself, but that's true of a lot of full-blown programming languages, too (especially the dynamic and weakly-typed ones).
If anything, a static configuration file should be easier to statically verify than a full-blown programming language because it's, you know, actually static. This goes out the window somewhat with YAML or XML (which do somewhat funny things), but once you have that hash table or whatever you've deserialized the config data into, congrats, now you can statically analyze it.
To give an example: you can get into the situation that some configuration values are only valid when configuration value X is Y, and otherwise other configuration values are valid. What better way to model this than through an if-statement in a programming language? This makes it immediately apparent which settings have effect.
If some config values are valid when X is true, and other config values are valid when X is false, then use a different schema for the two cases. This is what discriminated unions are for: for representing conditionals statically, inspectably, serialisably!
If some config values are valid when X is 0.339, and some are valid when X is 0.340, and some are valid when X is 0.341, and so on, then… I don't know how to help, and maybe I must just avert my eyes in shame as I implement the dynamic logic. (But in that case it seems a bit odd to say you're "telling the program what to do" with this configuration; I'd say you're bolting on a little extra program at the start.)
In 8 months I couldn't understand head nor tails of the configuration, where and when certain functions were used, etc.
It was (and is) my greatest disaster in writing a static website generator. (And I'd written 3 pretty decent ones before that). FWIW here's the code: https://github.com/ergo-cms
Also putting the validation logic directly inside the configuration file is not a good idea in my opinion for two main reasons:
* the validation logic is then "external" to the program and can be overridden (with potential catastrophic effects) by the operator installing the program.
* Configuration validation logic can be quite complex, and having it inside the configuration file would clutter it too much in many cases.
I tend to see configuration files as an API/Interface (maybe because I'm an SRE), and as any interface, I want it clearly defined, with known boundaries and as explicit as possible. For these reasons I'm not a big fan of using a fully fledge programing language as configuration.
The source code tells a program what to do.
Configuration tells the program about its environment.
My hunch is that it would be good with a different term than "config" for those things.
Configuration may be an overloaded term, but in my terminology telling machinery what to do is the function of the code. Configuration gives users, who in general have neither interest nor skill to follow the algorithm paths, some high level options to control the program execution. Thus config files need to be small, clean and well commented. Just my 2c.
To be clear complicate means to to make many while the word simplify means to make fewer.
That sounds like the opposite of what the article describes. The point of having configuration as code is having all code related to config in a single file. There is no config generation, nor config loading.
This makes no sense, because the program has to understand the config in order to use it. That means the code that defines what should be in the config and how it's structured has to be in the program, not the config.
Purely as an example, let's say I'm writing a program that wants to consume a list of files in a JSON config.
{
"files": ["/path/a", "/path/b"]
}
Now, let's say I want to pass in a wildcard pattern, or I want to exclude a specific directory, or I want to dynamically load the file paths based on a REST request, or some kind of other complicated crap.If I want to have a static JSON file as the config, I have a couple of options:
First, I can put logic inside of my program to handle wildcards, and to distinguish between filepaths and URLs, and to have exclude paths. This is not ideal because it makes my program much more complicated, and it's not even complication for a particular useful reason. 99% of the time I'm not going to need those options, so I'm writing all of this code to try and anticipate weird inputs that most people don't need. It also forces me to make opinionated decisions about stupid stuff like, "what should the glob format be? What Regex variant are we using?" None of these are questions I care about or want to answer.
The second option, which would be a lot better, is I could have a 3rd-party program generate a config on the fly, write it to disk, and then launch my actual program when it's done. This means I don't need to have a bunch of extra logic in my main program, and everything is a lot less opinionated.
BUT, it also means that the logic to launch my program is split up across multiple files, and now I'm adding some weird dependencies to my runtime. It's a hacky solution that's layering a bunch of complexity on top of what should be a simple, "launch the program."
----
So, what's a third option? I write my config file with code, which my program will execute, and which will return the original static config that's nice to consume.
module.exports = {
files: [
//some big list of files, maybe populated with a glob.
//exclude any filepath that contains the letter 'c'
].filter((file) => !file.includes('c'));
};
I still need the code to spit out an array of files at the end, but I can do that after making some network requests, or running custom filter functions on that array, or doing really whatever the heck I want.In many cases this does not introduce any new dependencies to my runtime. It's a lot less error prone -- I can launch multiple instances of my app at the same time without worrying about files overwriting each other. It's also a lot cleaner and easier to refactor, because all of my filter functions and Regexes and generation logic lives in one file.
And most importantly, for users who aren't taking advantage of the dynamic nature of the config, I'm still ultimately shipping a smaller, simpler program with a less complicated API. I don't need to ship a bunch of error-prone logic dedicated to reconciling 20 different flags or options from random pull requests.
----
Now, taking this a step further, you don't necessarily need to return a static data object. If I'm building a static site generator, maybe instead of embedding a bunch of logic in my system to load/parse Pug templates, instead I just consume a function from the config that takes a string and returns a string.
And the reason that might be attractive (beyond what I list above) is that the programmatic APIs for a library like Pug are already just as short and easy to use as any set of YAML options you can come up with. So instead of forcing your users to learn your new APIs to slot in a custom template engine, they just use the APIs they already know and enjoy, and your documentation is shorter and easier to read, and everyone wins.
And again, importantly, getting rid of that extra logic has reduced the amount of code in your main project and made stuff like validation and loading easier to reason about and easier to debug.
The way you want to think about a config that returns a function is the same way you already think about functions-as-parameters in functional languages like Lisp or Javascript. The trick is realizing that there is no real difference between a config file on a filesystem and an API for a library you're importing at runtime.
I understand the distinction you are making, but I don't think it's what the article is illustrating in its examples.
> I write my config file with code, which my program will execute
If you, the developer, are writing this code (for example, to provide some pre-packaged standard configs for different use cases, and the user just selects which one they want to use), that's one thing.
If your users are writing this code, you have a whole new set of problems that you don't have if users can only provide text config files in a safe format.
I can see providing a separate config editor program users can use to generate configs for your main program; you can write the code for that program to ensure that the generated config files are safe. But such a program would be generating configs separate from the running of your main program.
If your configs are really complicated enough that they need to be generated at the time your main program runs, to me that means your code needs refactoring. Either that, or what you are calling a "config" is really a plugin/extension/scripting system, where your users can write their own code and your main program is more of an interpreter for it. That's a whole separate use case that I don't think is well described by the term "config". Certainly I would say that's the case if your "config" can return a function to your main program.
I'm not sure that's true. In fact, I would go so far as to say for some programs, it's the opposite.
In the example I describe above (forgetting about lambda functions or anything like that) I'm shipping a program that consumes a small set of very rigid input options. I consume a single array of strings that need to resolve to file paths.
That's incredibly easy to validate: that's the type of limited input where I can be pretty certain that my program will not have any bugs or vulnerabilities around the input. It's also an incredibly simple API to teach to users, it's literally one option. And importantly, my program is still pretty powerful -- you can do whatever you need to do to get that file list without me needing to extend what input I accept.
I can't think of a good way to convert that to a pure JSON/YAML config without either making my software more error prone or making my software much less useful.
If returning lambda functions from a config file scares you, forcing the config to be a static object literal but allowing that object literal to be generated at runtime will still (often) result in a runtime that is less error prone, easier to validate, and easier to debug when something goes wrong. This is because at the end of the user's dynamic config, they'll still be returning a small, easy-to-understand object literal -- whatever logic was used to build it.
If instead you put that logic in your main program, and you accept globs for files, and you start doing complicated things like allowing arrays of multiple globs and file excludes -- well, suddenly you have a big chunk of parsing logic in your main program that end-users can't debug when something goes wrong. It becomes harder to validate whether or not you have some bug around the order that patterns are passed to you, or whether there's an error if you try to use a globbed filepath as an excluded filepath, or whatever.
In other words, it is usually better to use procedural logic to generate declarative logic than it is to use declarative logic to generate procedural logic. Of course, your program might be simple enough that you can get away with a config file that's turned into a bunch of `if` statements when its parsed. But it probably would be simpler if you got rid of as much parsing code as possible and focused on more rigidly validating a smaller, stricter set of options.
> I can see providing a separate config editor program users can use to generate configs for your main program
If you ignore the stuff about lambda functions, that's exactly what we're doing. We're just piggybacking on a real language to do it instead of wasting time writing our own graphical/command-line utility.
----
Where returning functions is concerned:
> Either that, or what you are calling a "config" is really a plugin/extension/scripting system
I think this is a kind of blurry line. I'm not going to say there is no difference between a plugin system and a config, but I have run into programs that have both configs and plugin systems, and where the plugin system exists mostly to deal with the fact that the config API is not very useful.
As a user, given the choice between learning one API and two, I usually prefer to learn one API. A lot of static site generators come to mind; I don't want to learn two different APIs for a templating engine just so I can change how links render. I don't want to learn both your plugin API and your config API. Just give me one, single API for templates that can be fully described in a single paragraph of text.
I dunno. I've been doing functional programming long enough that the idea of lambdas just doesn't scare me. Functional paradigms are wildly useful when building library APIs that get imported; I don't see why we should throw them away just because our API lives in a dotfile.
And I'm finding it very hard to imagine why your users would need to write code to give your program those paths, instead of just giving you a text file with a list of file paths in it. Of course I don't know anything about your program or its use cases other than what you've said here. But I do know that if your users are writing code, what you're describing doesn't look to me like a config. See further comments below.
> that's exactly what we're doing
Making your users write actual code is not the same as providing them with a config editor.
> I think this is a kind of blurry line.
I think the line is pretty clear: are your users expected to know how to write code, or not?
If they're not, then why would they need the ability to write code to generate your config, since they can't do it anyway?
If they are, then as I said, you don't have a static program with a config, you have a scripting language and an interpreter. Your program is just a customized version of the standard interpreter for whatever programming language your users are expected to use.
I don't have any problem with this if it's appropriate for your users. I just don't think the word "config" is the right one to use to describe it.
> I've been doing functional programming long enough that the idea of lambdas just doesn't scare me.
Whether it scares you or not is irrelevant unless you are the only one writing configs for your program. If your users are doing it, the relevant question is what scares them, not you.
But take a step back and look at a lot of the examples that people are bringing up here -- Docker, Babel, Webpack, Github Actions, code linters, testing libraries, Ansible. These are all environments where it's pretty reasonable to expect users to know how to write code.
If you're mostly getting hung up about me calling this a config, let me see if I can rephrase the original article's point in a way that is more palatable to you:
A lot of people are building programs explicitly for programmers that use config files, when those programs would be much better suited by instead dropping the configs entirely and using something more like scripted options.
These program authors struggle building a config schema that is powerful enough for programmers yet simple enough that programmers won't get irritated learning yet another multi-page API. What the authors have missed is that they shouldn't be trying to control their program with a config at all. Their entire product is geared at programmers, so they should just let programmers generate the options that they pass in.
There's very little reason for Babel to be setting options with a JSON config file instead of a JS script, because nobody uses Babel other than Javascript programmers. For these specific programs that are struggling to express themselves with YAML/JSON, a non-config scripted setup would be less error-prone and easier for their users to learn.
For those kinds of environments, yes, I agree that it makes a lot more sense to just have users write code in the same language that the main program is in, than to try to hand-roll a config file format and structure.
I didn't get the sense from the article that its scope was limited to these kinds of environments, but I may have missed something.
We've got a project in Python, but Python is too hard to read, so instead we have YAML. But YAML is too hard to write, so now we have YTT.
We've now taken a program that was written in one language, and magically transformed it into 3 languages instead :)
If you find yourself thinking that your config language isn't powerful enough, the solution is to either generate it, or to fix the program to not require a complex config file. There is a long term, 10 year cost to making config turing complete that I don't believe is worth paying. It'll save you time up front, maybe, but I certainly am not going to be impressed with having to modify your program just to "configure" it.
Finally, given that's the environment we're dealing with, from now on the rational thing to do would be to design config file formats expecting them to eventually get templated. Because that's how they get automated.
But... that's kind of what a dynamic config file is.
If you're writing a JS program that imports and runs a JS file to generate an object literal for its options, you have just made a config file format that you expect to be generated, or templated, or whatever you'd like to call it. The only difference is that you're using using 1 language instead of 3, and your users don't need to have a separate step during build to generate the file.
People keep on circling around to, "but what if my user doesn't know the language or isn't comfortable with it?" And sure, if your user isn't comfortable programming, don't do this. But if your user does know how to program, don't force them to learn a new templating format, just let them write the code that they already know.
Or honestly, just play the joke out straight and build a template engine for your source language. I get that you're making a joke above, but a templating engine that spits out valid Python/JS code from valid JSON/YAML is trivial to write.
var template_source = fs.readFileSync(process.argv[1]);
var output = JSON.stringify(JSON.parse(template_source), null, 2); /* check malformatted input and prettify */
fs.writeFileSync('config.js', `module.exports=${output};`);
So if you go this route and a few of your users end up wanting to template JSON/YAML anyway, then fine, they still can.or put another way, stop trying to reinvent the wheel and expect that OTHER people will script the generation of the config file, in their own way using their own tools and languages, and just make that easier for them.
when devops are involved, the question isn't whether they're comfortable with whatever language, the question is how easy it will be for them to template the file, because they simply will not learn your special configuration language, whether they possibly could or not.
The point isn't about whether we should make the automation people learn a new templating language. the point is they simply will not. they will find where the relevant parts of the config file are, and they'll stick a ${my_variable} in there, process it using their own tool, whatever it is, regardless of how cleverly (or not) you designed the scripting language around the config. And now whether your config file is python yaml or json is just irrelevant. They will not use python to loop through a file list, even if they could, or know how. They will template that. all you've done by switching your config to python is create a breeding ground for script injection attacks.
So ultimately it's the wrong question. What I'm proposing is, design our config file expecting the ${subsititution_variable} will get edited in at some point, and you guard against script injection assuming that is comimng.
I don't understand what you're getting at. This is just a 3 line executable you call when you're done with whatever templating process you want to use. Ship it with the program.
node generate_config.js path/to/json/templated/with/literally/any/tool.json
But if you really think that a devops engineer will struggle calling that command, then fine. Put these 7 lines at the top of your program. var config = (()=>{
try {
return require('./config.js');
} catch (err) {
return JSON.parse(fs.readFileSync('./config.json');
}
})();
Now your engineers can stick a `./config.json` in and template it with whatever automation tool they want in whatever integrated pipeline they want to work with. They never need to acknowledge that runtime option generation is possible. They never need to run even one extra shell command.So I just don't see this as a real problem. If your config script is ultimately returning a static object literal then there is no special knowledge with this setup that anyone needs to learn. All we're doing is giving people the option to script a config on a lower level in a real programming language. We're not inventing a new scripting language or toolchain. And it's still trivial for engineers to ignore that if they want to compile a config with a 3rd-party templating tool.
> What I'm proposing is, design our config file expecting the ${subsititution_variable} will get edited in at some point, and you guard against script injection assuming that is coming.
I would maintain that a config file isn't really where you should be handling security, that's something that should be ingrained into your program at a deeper, more fundamental layer.
But granted -- if for some reason your application can't be run in a secure unprivileged environment, and you need to accept untrusted config files within that environment, and you also can't run the config generation code in a secure unprivileged environment, then it would make sense to strictly limit a config file to a static format like JSON, where you know that no one else can inject actual code. But that's a very different concern than usability, and affects an even smaller subset of software than what we were talking about above. This is just not relevant for systems like Github Actions, Babel, Webpack, or Ansible, all of which are built on either the assumption that the config is trustworthy, or that the trustworthiness of the config doesn't matter because of sandboxing.
In most (not all, but most) cases, if you don't trust your user config, you're already at the point where you should be running your software in an unprivileged sandbox.
It's worth very seriously asking the question of WHY devops are doing this instead of just generating the yaml file with code, which from our perspective as developers would be easier and more sane, and allow us to use the full power of a programming language. I think it's a more important problem to solve than ergonomics of config for the original developers, because end of the day, who will need to interact with this configuration file more? Developers or the people responsible for deploying the application?
You said the kind of scripting system you were describing was for cases where your users can write code.
If your users can write code, I don't see why Python would be too hard to read.
If your users can't write code, then, as you agreed, you shouldn't expect them to, which means the kind of system you were describing won't work. And that means you should rewrite your code so it doesn't need such a complicated configuration that it seems like writing code is necessary to express it.
What I was getting at was that this is a scenario where concerns over Python users needing to actually use Python has morphed into those same users now needing to use 3 languages.
Thank god I no longer have to deal with sendmail.
This is the beginning of the logic that lead to things like Automake, and completely impenetrable makefiles. The problem with programmable scripted config isn't the writing of it. it's the reading and maintenance. Programmed config is a layer of obfuscation, that I, as a maintainer, need to spend time and mental effort decoding to get work done.
However, very specifically where Makefiles are concerned:
> This is the beginning of the logic that lead to things like Automake, and completely impenetrable makefiles.
The problem with Make in specific isn't scripting the build, it's that Make is a completely separate domain-specific language that isn't particularly well structured or particularly elegant in the first place.
The comparison I would draw is with Grunt.
Grunt is also a programmable config that allows you to do powerful stuff. It's also IMO a giant disaster and most projects shouldn't use it. Grunt is using programming to control how a project builds, but it's also tying tons of layers of abstraction into this secondary API that's unreasonably difficult to debug, and that lends itself to really bad, unnecessarily verbose control flows.
I was teaching interns how our build process worked at a company, and I had an "aha" moment when I realized that in the time I could teach them Grunt's API, I could also teach them how to write a maybe 100 line pure-JS script that did literally everything Grunt was doing for us.
When I look at Makefiles and Gruntfiles and cringe, very often the thing that makes me cringe are these giant pillars of abstractions and control flows of "program X calls program Y and pipes its output to program Z, and good luck intercepting that specific output to see what's going wrong".
There's something to be said for getting rid of these giant build systems that are spanning multiple processes and languages in general, and instead doing something very direct in one language in one file. And I kind of think that's a concern that cuts across different config types. At my current job, we're in the process of dropping Team City, because we realized that for our needs, we can get by with a single 30-ish line bash script that gets copied to each build machine and auto-run every 5 minutes. And Team City is using a lot of declarative options in a very structured web UI, but the Bash script is still a lot easier to debug and reason about than Team City's whole... thing.
Which is not to say you're wrong, I do think you're making a completely valid point. But I don't think the obfuscation you're talking about is inevitable, I think that's just a side effect of programmers thinking they're clever and trying to make generalized build systems for specific tasks.
If your users are writing this problem, you have a whole set of problems that aren't being solved if users can only provide text config files in a safe format.
based on Greenspun's 10th rule (Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of CommonLisp.), perhaps sufficiently complicated config system will contain a badly designed plugin/extension/scripting system inside of it.
The fraction of my dotfiles that is simply small scripts that “wrap” actual programs so I can do this is extremely high. In some cases I have resorted to code injection to accomplish this sort of dynamic configuration generation without putting a file on disk (yes, using fmemopen).
That said, the combination of SaaS and CI/CD makes that a bit of a challenge. Everyone wants to launch features darkly. You don't have to change your strategy to do that per se. You can move everything else into code or into a service discovery system, but the 'feature toggles' have to live somewhere, and they are essentially config.
If your CD system were ridiculously fast, you could just push a commit to turn something on and back off again. But I haven't seen many systems that are as fast as pushing a change into for instance Consul (even if you are using git2consul, that'd be faster than a typical CI/CD build)
The emscripten SDK (compile C/C++ to WASM) writes a config file as python source file when it is installed. At first glance it looks like a fairly simple key/value config file, but it cannot be parsed by a 3rd-party-tool (for instance to extract the paths to the installed emscripten SDK) unless the file is evaluated with a full-blown python interpreter.
I don't understand how this can be considered a "good idea" under any circumstances ;)
The answer to that is always an Irish Coffee.
Exactly. Even the simple toy example in the article illustrates the problem: why is the Person class being defined in the config? If the program is going to use objects representing people, the code defining the structure of those objects should go in the program. If the program is not going to use objects representing people, why are you adding extra code to your config to define Person objects when you could just use a list of plain tuples?
I suggest that all solutions to the config problem gravitate towards major suckage, but creating new micro-languages like yaml just seems like adding more complexity for zero benefit.
The advantage of config.py and config.js is that anyone claiming to be a programmer can read them and immediately grok exactly what's going on.
That said, starting now I would go with json as it is the lingua franca that every language has to understand -- and it is not a full programming language; you can't do conditionals or loops etc. Put that shit in your code please! Logic should never be platform-dependant.
Lets take this scenario. I want current weather value be part of config value. This could be pulled/scraped from online source in 2 lines of python. Alternative as you imply every program should have some complex abstraction for scraping now? That's just silly.
> To me it seems like this would lead to poor reproducibility.
I'm not sure how reproducibility matter here. You're not writing tests - it's a program on your computer for your personal use.
I think there's a divide between developer and user that could be bridged with a config.
Many, if not every piece of software in the world is crippled by what the developer allows you to do.
One example is the systemd config files. they are so brain dead that everyone has to go write their own logic to handle anything remotely flexible.
On the other hand, they're a clear limited user interface to systemd. bleh. who knows.
There is a real difference between declarative and imperative approach to configuration and such decisions can be traced in many projects.
Simplifying things: declarative configuration is usually the best approach if you manage to capture most of your component behaviour in some relatively simple language with limited expressivity.
Imperative is where you start mixing some code in. This is usually because of big amount of corner cases and where flexibility is a must. This is seen quite often in build tools: it’s hard to just configure everything, some projects need to be built using really custom pipelines.
And even if you don't want to implement logic in config file, there are still advantages in having access to a full language.
For example: with a simple config file
maxAttachmentSize: 33554432,
maxTotalSize: 33816576
with a full language maxAttachmentSize = 32 * 1024 * 1024; // 32 MB covers 99% of use cases
maxTotalSize = (256 * 1024) + maxAttachmentSize; // 256 kB of text + one big attachment
No logic here, just a more readable file. But that uses several things json doesn't support: comments, arithmetic and variables.As a bonus, your config is more readable.
maxTotalSize: 1MB
Or similar.
maxAttachmentSize: 33554432 # 32MB
maxTotalSize: 33816576 # 256kB + maxAttachmentSize
All the "full language" version is done is avoided me having to use the incredibly powerful calculator I'm sitting in front of to calculate some numbers for all of 5 seconds.Programatically solving something which changes very infrequently isn't really a good case for all the other complexity and potential issues something like this would drag along with it.
That works until person 1 updates the value, but does not notice the comment duplicates it, then person 2 spends hours figuring out why he can't upload 30MB file, while he can clearly see in the config, that it allows up to 32MB, and his debugger tells him limit value actually matches that long number.
Your example is fine, it’s certainly more pleasant than not doing it that way, but that’s not where the trouble starts: the trouble begins when you have conditions and loops and any non-trivial logic.
If you need non-trivial logic then it probably isn’t “configuration” and should be part of the normal codebase (and go through the same review and testing procedures).
I would also argue that if your system is so complex that you need a programming language to configure it, there’s something very very wrong.
SAP has some 3,000 database tables that are all configuration tables. Consultants are paid $200-$250 an hour to do nothing but tweak all these settings.
It sucks that SAP was designed this way but god damn its an easy way to make bank.
You're dismissing a viable solution that works extremely well. Take Elixir for example, which is configured using Elixir
https://hexdocs.pm/elixir/master/Config.html
Its simple, powerful, and allows you to make it as complicated or not, as you want.
- Emacs which uses elisp an embedded Lisp interpreter to configure but as well to implement new functionality.
- XMonad, a tiling Windows Manager written in Haskell and it is configured inside the program itself, i.e. you edit the main program.
- Guix package manager (and GuixSD, the distribution) that uses Guile (a scheme) to describe the programs to be installed and their configuration. This case is interesting because Guix is using the same concept/functionality to manage the package dependencies which they took from the Nix package manager, but in contrary to Nix decided to use the general purpose scheme programming language instead of a special configuration language.
For instance: I'm mostly familiar with Jsonnet, which has guarantees that make it much easier to use than a Real Language: no arbitrary file loads (paths must be static), no side effects, no ambient state (like env vars or Internet). Relative imports make it very easy to drop in the language somewhere in a repo and not have to worry about venvs/PYTHONPATH/system libraries... The interpreter being a single binary and a thin C library also simplifies integration quite a bit, compared to interfacing with full-blown interpreter/compiler installations.
The downside is, of course, that you must learn a new language. But we're all pretty good at this (unless you're one of those people obsessed with using JS/npm everywhere for some inexplicable reason). Also Jsonnet still has the pedigree of being a superset of JSON, which is painful (ie. magical int/float number type). But the alternatives (Dhall, Cue) fix that, and also provide a vastly better type system.
I've been using Jsonnet in production for a couple of years (after leaving Google where I Saw The Light with BCL), and couldn't be happier. The infrastructure of the Warsaw Hackerspace (a production k8s cluster) is all brought up using Nix and Jsonnet and is open for all to inspect [1].
And yes. Let's please stop using JSON and YAML for everything. Even Python is better than yet another YAML templating tool.
[1] - https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/refs...
Edit: here's a thesis that works with a related language, GCL, with code samples: https://pure.tue.nl/ws/portalfiles/portal/46927079/638953-1....
I have a different approach. When I'm deciding about a new technology that we could add to our tech stack, I ask myself if it's really worth it. Adding a new tech is very expensive because it adds to the list of things that the developers have to be good at. I would avoid adding a configuration language and prefer using the same general purpose language that the rest of the software is written in.
At work, we have a C# ASP.NET Core application we build.
- We use Pulumi's C# API to deploy to the cloud. This lets us avoid yet another YAML based config format, and it lets us edit the deployment in a familiar language with a pretty simple API in our favorite IDE. (Link: https://www.pulumi.com/)
- We're going to move to Nuke instead of having Azure Pipelines YAML files. One less YAML file format to remember. (Link: https://www.nuke.build/)
- We're going to do benchmarks with BenchmarkDotNet, to keep our code in C#. (Link: https://benchmarkdotnet.org/)
- We're going to be adding Spark-based analytics, and I'm undecided about whether we should use Python or the C#. Python is so ubiquitous in analytics and there are so many libraries that it might be worth adding another language to our system. (Link: https://dotnet.microsoft.com/apps/data/spark)
EDIT: parent originally asked 'we use Pulumi, have you heard of it?'
Yeah, I'm also trying to find the magical bcl/gcl - but what was so useful (when I worked at google) was the diff tool (was it written by an intern, it was amazingly useful) - e.g. your presubmit would tell you - how the end (evaluated) result would look like, as one change in one place might have quite the ripple effect.
I haven't tried that yet. It might work. I mean, I've used starlark as _a_ configuration language (notably, in Bazel, or as a little prolog-standin for defining complex rules), but never really built an entire infrastructure on top of it. I would have to look into how it works my usecase (can't lie that jsonnet having an existing third-party kubernetes library was a big seller for me, even if I end up extending it). I do prefer, however, functional/lazily evaluated/declarative languages for configuration - while starlark has a very imperative taste to it.
> [...] but still felt the evaluation time was higher [...]
Yeah, that can be painful, but has not fully bitten me - my configs rarely get large enough to notice, and even if they do there is a large bottleneck somewhere else (ie. actually applying the config). Definitely also have it on my backlog to look into sending some patches for that.
> was the diff tool
There exists something like this, kind of, for when you're using jsonnet to emit k8s resources: `kubecfg diff`. But that's for diffing against prod, not an evaluation against a previous VCS tip. Definitely a tool that should exist, though :). I do think Dhall is a bit further down the road of having this sort of tooling actually exist upstream.
I'm not convinced - the ergonomics aren't great, it does not support CRDs, is mostly untyped, and you end up unnecessarily encapsulating everything with dozens of function parameters since you cannot easily merge nested data.
Starlark is great for configuring an imperative tool like Bazel which is only declarative at the top level, but is impractical for pure configuration data. Having the full power of a general purpose language isn't great for configuration - it quickly leads to lots of complexity at the wrong layer.
Cue has a radically different approach[1] that works very well in practice. The author of Cue, mplv, wrote the first version of bcl/gcl and Cue is the spiritual successor of sorts :-)
It's still early, but many people (including me) already use it in production. Their Slack is very friendly and helpful!
This all sounds like it should be a part of the sandbox, but it's not clear why it actually requires a whole new language. Many existing runtimes can be sandboxed in a similar manner. So why not just sandbox e.g. JS for JSON?
If you feel the need to put code in your runtime config, that means your design sucks.
For instance: I just brought up a new Factorio game server yesterday, just by adding a single line to my jsonnet configuration machinery: https://gerrit.hackerspace.pl/c/hscloud/+/246 . This in turn brought up a bunch of new k8s resources, including some persistent volumes, a deployment, and a loadbalancer service, after running a single `kubecfg update` command.
In your scenario, it's more like provisioning of infrastructure and in that case - configuration is the code, so I can see where you are coming from.
I mostly write financial software so the use case is usually closer to classical single machine applications. And in that case, I prefer simple TOML.
edit: Now I understand that being a plugin it must have proper abstraction, but ad-hoc logic do exist.
> ... but ad-hoc logic do exist.
True. I don't disagree, no tool is right for every job.
Should it be? If an application doesn't have configuration, then the logical thing to do would be hard code all the values it needs in the code itself. This could be done with a number of variables/constants defined in the entrypoint method for each value needed, or defining a data structure that contains all the values needed in the same method.
Reading a config file and serializing it into those variables or data structure shouldn't be a hard problem. Formats like YAML or json allow for deserailizing the string into a data structure (though you have to add your own validation). ini format files can be done the same way in a loop, but still require adding validation.
The validation could be based on data type or allowed values. While that's rather tedious, it's not complex.
No, but static config files won't give you that proof either. Unless I'm misunderstanding your point?
At least if I write build scripts in e.g. ts-node I can use the type system to provide some modicum of safety, which seems like a plus.
If your tooling requires static configs, but you really need dynamism, you’re stuck with dynamically generated static configs, which are even worse than a dynamic config.
The problem here is of course how not to make your DSL suck, which Jenkins' did not avoid. But if you're lucky, your language of choice already has a few libraries implementing embedded DSLs for configuration, and it's quite possible that at least one of them is actually usable.
Well, the choice is Java, or Java and yaml.
> The amount of complexity introduced just for configuration is insane.
Again, it's Java, vs. Java + Spring + env + application.yaml + application-$profile.yaml (in my case usually).
Once config constants enter your language, they're type-checked. Tree-structures suddenly need to be well-formed. No more trying to remember if you force https-only with 'enforceHttps', 'enforceHTTPS', 'enforceTLS', 'enforceSSL'. (= True,= true, :true, :1, :on, = "on")
> no regressions under different configurations.
It's great for that! Instantiate three copies of your service from Main - say, one backed by a hashmap, one connecting to a live db, one connecting to your docker-compose db, etc. Run the the same set of commands (in the same language your tests are already written in) and assert the outputs.
I don't want to do step 2 unless I can do step 3.
It just so happens that type-checking happens during recompilation, and testing happens after.
See ansible and the YAML config files.
In this case, a real language would have been a better choice.
Also, sometimes you need to generate config files. In that case, templating is not always a good choice.
https://www.gnu.org/software/make/manual/html_node/Functions...
Naturally, trying to do anything even slightly complex with it quickly leads to an unreadable cacophony of dollar signs, backslashes, and soft and hard tabs (you need both!). Just like the modern YAML-based systems, Make’s language wasn’t originally intended to be a full scripting language, but had scripting bolted on because people needed it. If only it had chosen to embed a real programming language instead, you could have the functionality without sacrificing readability.
It's ad-hoc because it was built gradually on top of the original Unix make, which supported rules and variables but not any of the more advanced features. This history explains many of its quirks and relatively extreme limitations as a programming language, such as:
- Expressions must be all on one line, except within `define` blocks or if you use backslashes to join lines together (which tends to result in a lot of backslashes).
- Hard tabs are required to introduce commands in rules but are (mostly) banned outside of them, so if you want any kind of indentation for an if block or user-defined function, you need to use spaces instead.
- User-defined functions. Calling them is merely more verbose than calling builtin functions (you need to invoke the `call` builtin function). Defining them is worse: function parameters are numbered rather than named, similar to a shell, but in a shell you can reassign them to named variables, whereas Make's expansion rules make that awkward to achieve.
- And of course, no types other than strings (a limitation partly shared by shells, but most shells do have arrays, even if they're awkward to use).
Admittedly, most of that gradual evolution happened a long time ago, and GNU Make has been relatively stable as a language since then. It's not "ad-hoc" in the sense that it's constantly changing or ill-defined. But that stability also means that it never outgrew the limitations of its original design.
Some research I did for fun:
The oldest version of GNU Make I can find [1], from 1988, already had a handful of functions, including `foreach`, `filter`, `patsubst`, etc., as well as support for multi-line definitions (`define`/`endef`). `call`, on the other hand, didn't appear until 1999. Amusingly, its initial implementation came with a comment [2] bemoaning the lack of "nested lists and quotes", and even semi-seriously proposing that GNU Guile (Lisp implementation) be integrated into Make, in order to let Makefile authors use a 'real' programming language. No fewer than 13 years later, that proposal actually became a reality; unfortunately, it was an optional feature which distributions tended to leave disabled, ergo Makefile authors could not rely on it being present, so the feature has seen approximately zero use.
[1] within https://gcc.gnu.org/pub/binutils/old-releases/binutils-1988-...
[2] https://github.com/mirror/make/blob/c4353af3f9b143b213644c09...
Also, Bash is a scripting environment (sort of), and VimL is a solid part of vim. These are scenarios, where you don't add undue complexity or bugginess to the software. But personally, I wouldn't kid myself and pretend that I can write applications as solid as bash or vim.
Makefiles are just bash scripts with dependency resolutions. So again, scripts and not configuration.
Well, the same thing can be said about pretty much anything. I've seen good and bad Django setups, so what?. The point is configuration as code works well if you know what you are doing. A config minimal language (toml, yaml, etc) may also work well as long as you don't need something that's not supported by it, and as long as you don't need to debug it.
If you are willing to accept the defaults you aren't required to write any configuration.
Hello from every plugin system ever. True fact: not all configuration is basic.
And usually these things are implemented with way more forethought.
I actually agree with you. Elsewhere in the comments I say that the OP article failed to name this properly: https://news.ycombinator.com/item?id=22788696
But it is what the article is really discussing.
I would look at Terraform and beg to differ - they started with a simple stringly typed language and had to build a typed language. Still no support for conditionals, so everyone has to abuse the only bit of control flow on offer - ternary variables and counts.
CircleCI is just as bad - it's a nice yaml format until you want a conditional step in workflow that's conditional on something other than the branch name.
It's very risky to settle on a static config at design time since the users will find a need. How would you cater to that user without offering a programming interface to configure it?
As we seem to be heading towards immutable-'everything' though, sometimes I wonder how valuable dynamic (in the sense of picked up at start) configuration is for backend services. It seems preferable to build everything, config included, into one single artifact. Potentially this gives more optimization opportunities as a lot branches could be completely eliminated (feature flags, etc.). No doubt the simplest way to build your config directly into your code is to just.. have it be code.
A potentially super annoying trade-off is that it's difficult to test minor configuration tweaks, and the annoyingness scales non-linearly with your build time.
If you have no way to hand off your TCP connections gracefully, that is probably not a great position to be in because computers are unreliable and sometimes you need security updates. One approach I tried experimentally for a TCP server (but unfortunately did not have the opportunity to try in production) was to have a small server acting as the frontend that handled sessions and called into the real implementation which was essentially stateless. This small server could theoretically support self updating without dropping connections, if need be. (The main reason I was doing this was so that the rest of the stack could be dynamically-scaling based on Kubernetes, while this part remained relatively static.) I think anytime you have forced statefulness, it’s worth isolating as much as possible so it doesn’t constrain the design of the whole system.
(Even better, of course, is to just have a protocol where clients can gracefully handle draining and reconnect without side effects.)
> ACL changes, quota changes, etc
At a certain level I think these are runtime data and not configs. Aforementioned service also dealt with ACLs and configs and we were serving it through another service that persisted to a database layer. One cool thing about doing it as a service is that you can manage and scale it like any other service.
I think that's the gist of the issue - sometimes it's difficult to tell which one is which :).
And I agree with the 'TCP connection in thin server' approach. Thankfully, I only have to deal with two protocols where this is an issue: IRC and BGP.
Absolutely... in fact, there's definitely stuff that I've had start as configuration and gradually turn into runtime data, and there are things I would call configuration that need to be dynamic at runtime too... I'm never 100% satisfied with the split.
No, I can’t identify what’s wrong with it. Why am I spending what feels like 5 minutes studying a code sample to find a flipped ‘I’ and ‘L’?
Every time I read about dahl, I remember this and it sours my patience for the language.
It’s a good point, but there’s got to be a better way of communicating the value. Or, maybe just having a button that says “spoilers” and reveals the issue.
The whole story can be found here: https://www.lua.org/history.html
The Lua code made it easy to configure things such as the path to sendmail, and your email address. But also writing functions to filter, colour, and display messages was a joy.
mutt is awesome, but the configuration language it uses is a mess of history. No loops and conditionals being somewhat limiting. People end up writing "profiles" by essentially reading the configuration values from a different init-file. Horrid.
Kotlin and kotlin script seems to work reasonably well for Gradle. Gradle unfortunately has a bit of groovy legacy, which makes it a bit convoluted for some things. But Kotlin DSLs are quite nice for configuring things and it removes a lot of ambiguity when your editor can tell you a property doesn't exist or that a list instead of a string is expected. or help you autocomplete things that are allowed.
Basically the ultimate in configuration languages is something that has a minimal syntax but provides enough structure for tooling to help you validate things, provide autocomplete and other help, etc. There are now several languages emerging that have both static typing and some level of support for internal DSLs. Ruby sort of pioneered a lot of configuration DSLs back in the day but since it was dynamically typed, tool support for these DSLs was a lot harder. With languages like Kotlin, Swift, C#, Rust, etc. you get enough expressiveness that you can do similar internal DSLs but with the advantage of more robust tooling support.
With gradle, there's three big phases: initialization, configuration, then execution. (See https://docs.gradle.org/current/userguide/build_lifecycle.ht...). I'd think you'd need to do something similar with any structured configuration DSL - have distinct phases where you may "init, plan, apply", etc.
This, it's a pretty complex problem to have. But that complexity may be worth it as part of a "developer productivity engineering" effort, where you're really looking to use data to minimize the time between a configuration change, and the time it's applied in a runtime environment. Doing that is a hard problem.
From that perspective Groovy is much nicer ...
Bazel's configuration language is called Starlark[0] (used to be Skylark). It's not a strict subset of Python. It has implementations in Java[1] and Go[2]. I haven't had a chance to use it yet, but it seems very useful as a general-purpose scripting language, especially for embedding into tools.
[0] https://docs.bazel.build/versions/master/skylark/language.ht...
[1] https://github.com/bazelbuild/bazel/tree/master/src/main/jav...
The mapping to and from Go types is a bit obtuse and could probably be smoothed over by reflection or an intermediate representation like JSON in many cases. It's not just data though: you can also send functions in both directions.
Embedding an interpreter for filtering vs. trying to compile all possible filtering needs into the binary greatly improved my program's power and shrunk the volume of application code. I recommend it.
Don't be alarmed by the Bazel connection: it's much easier to use and understand (and better documented) than Bazel itself. Although I am a lot better at Bazel now for having taken the time to understand Starlark.
All that said: if you find yourself needing to support logic and behavior in your configuration language, you may not actually have a good enough abstraction to justify a config-driven system. Take a hard look at whether it might be cleaner to write plain old code, or APIs for other people's code. A significant number of internal platforms at my company are unnecessarily obtuse because they try to put everything in boxes like this vs. being legitimately composable/programmable through ordinary software engineering tools and processes.
My best example would be a CI system, given a CL (changelist) it can tell you exactly what targets need to get built. In our current CI - we have put extreme whitelsits - e.g. if you have touched this folder with C++ files, these are the .sln files to build. But can't move to bazel yet (mostly Windows shop). I actively use it for my home projects (linux + windows), but that's about it.
`load` does magic, so that `load('//path', 'name')` puts `name` into the file scope, which is possible in pure python but ewww.
Containers are immutable, so they can go in default values, which is good since a lot of bzl and BUILD relies on default arguments for brevity.
The mental lines between Starlark and Python are fully blurred at this point, but I've seen the "Starlark isn't Python" a few times this past week already.
Thanks for the elaboration :)
I guess my point is it's so similar to Python that in most cases I don't even notice.
This avoids the security problems and the risk that someone will start to write programs inside the configuration file, but gives you the reasonably nice and powerful syntax.
It turns out that teenagers in my country have often done some Python in school, so it's becoming one of the friendlier file formats for non-programmers.
Yes, they are not general. Still, having one dedicated place for all constant switches is much better than constants being out in a dozen (or hundreds) of places all over code.
You don't _have_ to test anything, just like in any software development. But now you can, and you can do it in a side-effect-free environment, on CI push, instead of realizing you made a typo when the CM kicks in, or worse, when your system starts crashlooping on startup because of a configuration issue.
I, personally, would rather prefer straightforward static configuration and very well written documentation on why certain configurations make more sense than others. That way, I know how and when to change something in the configs.
I absolutely do not share the view that "tests are documentation", because they do not communicate the most important thing in software engineering - the intent and reasoning behind a certain design.
This would be a sign of a program that allowed input (configuration, in this case) that was unsafe. That is completely a different matter to whether a programing language is suitable for defining configuration. I suspect that many an application has been written that assumes that because it's consuming "static" configuration that is a simple text file, or a structured data file, that the input is completely safe. (SQL injection can come from any direction.)
- doesn't have comments
XML uses the same <!-- stuff here --> SGML-style comments that HTML does
- bits of configs can't be reused
XML can include other XML via XInclude: https://www.w3.org/TR/xinclude/
- can't contain any logic
You can technically put logic in XML using XSLT (https://www.w3.org/TR/2017/REC-xslt-30-20170608/), but I would ask if one SHOULD put logic into your configuration. I personally would recommend against it if at all possible. Keep configuration as declarative as possible and put the logic in your program. It's better to have a configuration logic module or use dependency injection than put logic in your config.
- Programming language constructs are reinvented from scratch
Again, XSLT could be used, but should your configuration really need to be Turing-complete? Granted, XML is complicated and not without criticisms, but it is mature, standardized, and well-supported. Python has great XML support: https://docs.python.org/3/library/xml.html
- can't be validated
XML can be validated against XML Schema (https://www.w3.org/XML/Schema), Relax NG (https://relaxng.org/), and possibly other standards.
- implicit conversions and portability issues
XML can be used portably between different platforms and programming languages depending on how the data is represented. XML Schema defines a way to describe data types that is flexible and interoperabile: https://www.w3.org/TR/xmlschema-2/
Again, I know it's not the popular answer, but I was surprised that it was not even mentioned in the article.
<httpd>
<baseDir>/opt/server</baseDir>
</httpd>
Of that snippet, "</baseDir>" and "</httpd>" are characters of my life that I will never get back, since they're just parser niceities and not _value_Moving up the enlightenment chain:
{"httpd": {"baseDir": "/opt/server"}}
or its brace-and-quoteless friend: httpd:
baseDir: /opt/server
place a lot more emphasis on the payload and less on the packagingWith XML and other "fully qualified" syntaxes the named closing tag makes it easy to visually see where a block starts and stops. Humans parse the text too. This begs the question: Do you read config more or do you write config more?
Braces and parens all look the same, so its harder to visually match them. "But wait," you may say, "my editor / IDE does brace and paren matching so I can easily see and jump to the corresponding brace or paren." True, but doesn't your editor / IDE also support or have plugins for automatic HTML / XML end tag insertion?
Personally, I am equally comfortable with XML and JSON style configuration, but I find the brace-and-quoteless style unsettling because I believe that whitespace is a poor choice for delimiting structure. Your opinion may vary and that is fine. Do what works best for you.
<httpd baseDir="/opt/server"/>
and it is shorter than the JSON.Expressiveness and flexibility are good so that you can define configuration to meet the needs of the system or application you are building.
XML Schema, Relax NG, etc. can be used to specify how the XML configuration should be structured, limits on data types, required versus optional configuration items.
As I said before, XML gets a lot of flack for being verbose, ugly, and complicated, but it is mature, widely-supported, and might be worth considering depending on your needs.
This is well-formed xml and good way to confuse people:
<?xml version="1.0"?><!DOCTYPE abc[<?abc >]]<abc><abc/>]>>?>]><?x?><a/><?x <(x)>>?>A few of OPs suggestions are not Turing complete, but rather total programming languages as is the case with Dhall.
Well, definitely not XSLT.
For example, years ago I was a frequent user of the chemical kinetics code called Cantera. It calculates the dynamics of combustion reactions, with the big application being for jet engines. One of the files that it needs to load is a mechanism file (called the CTI file). This contains all of the information about the gas properties and chemical reactions. Different situations might require different mechanisms (propane mechanism versus JP8 mechanism). Anyhow, Cantera's mechanism file format is literally a Python script. See the link below for the most commonly used mechanism file that comes with Cantera:
https://github.com/Cantera/cantera/blob/master/data/inputs/g...
This file is 2000 lines long, and many mechanism files are even longer. I told my colleagues that it is possible to execute arbitrary Python code using the files but I was unable to convince them that it was a security risk. I think that these kind of config files are a big security risk for engineering firms, because they make it much easier to conduct industrial espionage. All that a bad actor has to do is put a few lines in one and get an engineer to run it once. Then they could steal designs, analyses, business plans, financial data, and many other things. It's a serious threat that should not be minimized.
A common solution to this problem is to invent some kind of templating or pre-evaluation of your static config file. But I find these solutions quickly externalize a lot of complexity and are frustrating because it is often difficult to debug their evaluation.
At the point you want to do programming-like things in a config file, you might as well use a "real" programming language. Yes, it is complex in its own way. But if your target audience is programmers, I think it is an easy decision to justify.
I'm extremely happy with Starlark and PyOxidizer's configuration files are vastly more powerful than the TOML ones were.
I don't quite understand this part. Any examples?
In PyOxidizer's case, I wanted to create virtual pipelines of actions to perform. In TOML, we could create sections to express each stage in a logical pipeline. But if you wanted to share stages between pipelines, you were out of luck. With Starlark, you can define a stage as a function and have multiple functions reference it because is "just" calling a function.
I suppose I could have defined names for stages and made this work in TOML. So let's use a slightly more complicated example.
PyOxidizer config files need to allow filtering of resources. Essentially, call fn(x) to determine whether something is relevant. In the TOML world, we had to define explicit stages that applied filtering semantics: there were config primitives dedicated to applying filtering logic. PyOxidizer's config files had to expose primitives that could perform filtering logic desired by end-users. By contrast, Starlark exposes an iterable of objects and config files can examine attributes of each object and deploy their own logic for determining whether said object is relevant. This is far more powerful, as config files can define their own filtering rules in a programming language without being constrained by what the TOML-based config syntax supports.
I do not think I would use this author's suggestion though.
I was recently thinking that its a shame there isn't a cross language proto-lite kind of thing (I'd settle for json with structures and pointers) that has a great devx. The multi step transform that configs are right now (from the serialization representation to the code one) could be eliminated. IMO interop is one the most important things for configs, so I'd be hesitant to use a thing that needs a preinstalled runtime.
To me, from best experiences to worst: Real language (ruby, Python) > Yaml/Toml > ini config > JSON > DSLs
Created by people who have dealt with this problem at scale for many years. Picked it up a month ago, absolutely amazing.
See here for more details: https://pure.tue.nl/ws/portalfiles/portal/46927079/638953-1....
It ticks off most of the author's boxes, but it's so woefully underspecified and so horrifyingly complex, that Google has been trying to replace it with something else for the past decade, unsuccessfully (at least as of a few years ago, don't know about now).
But you get to provide external parameters, compute values, use some rudimentary logic, inherit configs, reuse configs in larger configs, and so on and so forth. You can also spend a couple of weeks trying to understand the config structure of something like an ads backend.
You do get some niceties though: even fairly large services, consisting of dozens of different backends/mixers/frontends can be brought up/upgraded/shut down/reconfigured with a single CLI invocation, soup to nuts, including things like monitoring and load balancing.
Specifically, the project uses TypeORM and it was nice to be able to import the types for the DB connection options to make sure I had all of the necessary properties.
I was using ts-node to run server-side without compilation, which made importing an `app.config.ts` file quite elegant, IMO.
I'm generally against the idea of complex configs. Configs should be simple. Sometimes that requires extra planning or thinking from developers. Every time I've allowed logic in config files, I've regretted it.
The main annoyance about JSON is lack of commenting ability. This is a point I agree with but not a big deal IMO, it's still better than all other alternatives.
JSON is ideal in terms of being both machine-readable and human-readable. Some people would argue that YAML is more human-readable, but it's definitely less machine-readable. With YAML, there are too many situations where some random code somewhere will remove all the new lines and tabs (for whatever reason) and mess everything up. JSON is resilient to machine sanitization. JSON is simple, robust and readable.
https://mikehadlow.blogspot.com/2012/05/configuration-comple...
Hacker News discussion here:
Of course, if one has already access to a Python runtime that is probably fine as well.
https://android.googlesource.com/platform/external/chromium-...
https://googleprojectzero.blogspot.com/2017/12/apacolypse-no...
It's a nice balance, I think - most of the time you can treat it as a normal config file, but if you need to drop into some programmatic stuff you can. I've done this in my own dotfiles to add custom package definitions before contributing them upstream, for example: https://github.com/jfrederickson/dotfiles/blob/master/guix/g...
eslint or maybe jslint started off with json and later ended up letting you make a JavaScript file. As a simple example of something you might want to do, read the version from the package.json and generate a banner or an installation version.
Grunt (not sure who still uses Grunt but I do) is JavaScript based. And yes, sharing parts is something I commonly do as in define common options and re-use them in dev, production, or minimized, un-minimized.
WebPack takes a JavaScript file
For most build tasks I've had some place where I needed to use code to make the build better but it can go both ways. The worst experience I've ever had is Scons (python). No one I know ever really understood how it worked and so every programmers wrote wrong and hard to understand code trying to insert their special needs into the build.
> bits of configs can't be reused. For example, while YAML, in theory, supports reusing/including bits of the config (they call it anchors), some software like Github Actions doesn't support it.
I use YAML configuration for Home Assistant all the time, and the includes work just fine. This article is off to a really weak start by saying "config files can not have X, because well they can just fine but arbitrary software Y doesn't support it". Especially damning because the proposed solution is to embed a whole other programming language within your program! If you can change the code of the consuming program, then just add support for imports, instead of adding a Python dependency.
These are config files. A comment is something that gets ignored, this "comment" will be ignored by whatever ingests this JSON. If not, there are other problems. If you have to write a treatise here, there are other problems.
{
"comment": "This is a comment",
"dev": { ... },
"stage": { ... },
"prod": { ... }
}
Oh no, that line doesn't start with a # or //, whatever will we do!- you might not be certain if you break something with such a comment (i.e. you have to double check that all the "comment" fields are filtered)
- you're not geting highlighting from your text editor, it's easier to miss a comment
- you can't have a comment for a list item
Basically, you can leave the comments, but you're really discouraged to do so. Most people would just give up and not bother.
The following however
{
"comment": "Some comment",
...
}
Suddenly I'm not sure if this is meant for me, or if it's actually a configurable state without going into the program itself and checking if it uses it.A lot of mainstream applications come with documentation in the sample config file that explains what each setting does and what its default value is. For example, this is the default configuration for redis[1].
Having that information in the config file guides users in terms of what's possible to change and what values can be used.
[1] https://raw.githubusercontent.com/antirez/redis/5.0/redis.co...
Also, executable configurations are not uncommon. Consider .bashrc or .vimrc. And they're usually loaded with multi-line comments.
Embedding comments in JSON fields gets unwieldy really quick for long configs. Compare:
{
"generalcomment": "the quick brown fox\njumps over the lazy dog",
"setting1" : {},
"setting1comment" : "this is setting1",
"setting2tree": {
"setting2a": {},
"setting2acomment: "this is supposed to do this",
"setting2b": {},
"setting2bcomment: "this is supposed to do this"
},
"setting3": [1,2,3]
"setting3comment": "to use this setting, do this"
}
To: // The quick brown fox
// jumps over the lazy dog.
{
"setting1" : {}, // this is setting1
"setting2tree": {
"setting2a": {}, // this is supposed to do this
"setting2b": {}, // this is supposed to do this
},
"setting3": [1,2,3] // to use this setting, do this
}This is hard to believe. Go ahead and open any linux config file.
Then I'll have a python module config.py that reads that and injects those settings with perhaps a small amount of logic into that namespace on import. So you where ever you might need a setting you can just "import config; config.foo" to get a specific config value.
For small systems, I even do service discovery this way - where does the db live? config.postgres.host/port or something yada yada.
`psql -h $(jq -r '.services.postgres.host' config.json)`
1. Use a general purpose language (or ideally a very dumb subset like Starlark) to generate the configs. You get all the reusability, type safety, comments etc. 2. Have #1 output a deterministic config like json/yaml/etc, so you can always refer to what the config actually looks like without needing to debug the code in your head. Check that in and whoever uses the config only uses the static output, they shouldn’t care how it came to life.
This way a config is easy to reason about both on the author and consumer side.
That being said, these config can get very complex and I can’t imagine expressing them in something even relatively friendly like yaml
""" ... embrace the inevitability of programmatic configuration, and maintain a clean separation between computation and data. The language to represent the data should be a simple, data-only format such as JSON or YAML, and programmatic modification of this data should be done in a real programming language, where there are well-understood semantics, as well as good tooling ... """
Their philosophy is to use minimal software and all configuration is done in a single programming language file. But it's also written in C which means you need to recompile the app to change config values. They typically provide a bunch of patch files that you can optionally bake into your custom binary, or you can write your own modifications too.
This works for a user using one of the suckless tools like dwm or surf. However, for server side applications there maybe multiple configurations and it may not be feasible to compile the whole app for every environment or deployment.
If you program requires configurations so dynamic that it needs to be its own program, there's something very smelly about your architecture. Next thing we'll need is a config config to config your config, etc. Infinite regression into a very stupid place.
If you use a programming language, you had least have a way to scope.
God, what are we being punished for?
The sensible solution is to treat a makefile like code - document it and use sensible coding practices like organizing it instead of writing spaghetti soup.
It fills the same niche as make, but is a pure functional programming language, reproducible, and has a large package repository.
It's pretty much my favorite piece of software because it allows me to depend on third party packages with minimal risk of breaking the build.
* Not turing complete.
* Imports/exports
* Comments
* Variables
* Statically typed (amongst other benefits, allows you to use it for RPC, like protobuf)
I've started implementing such a language 2 or 3 times, but JSON is just barely good enough that I haven't been able to justify the effort to flesh it out. Taking JSON and adding types (inline in the grammar; schemas aren't good enough) and comments would get us 80% of the way there.
> Dhall is a programmable configuration language that you can think of as: JSON + functions + types + imports
We also do take things seriously, including:
* Creating a language server (do any other configuration languages have this?) * Soliciting donations to fund high priority work (https://opencollective.com/dhall) * Working on a book (https://github.com/Gabriel439/dhall-manual) * Maintaining a formal language semantics (https://github.com/dhall-lang/dhall-lang/tree/master/standar...)
If you think we're still missing something please let us know as we are responsive to user feedback
https://github.com/MonoidMusician/dhall-purescript
PureScript corresponds pretty closely to JavaScript in terms of the code it generates, so once we have a PureScript binding it shouldn't be hard to generate lean JavaScript bindings from that. Also, more people requested the PureScript bindings over the JavaScript bindings anyway in our most recent yearly survey:
http://www.haskellforall.com/2020/02/dhall-survey-results-20...
The best way I can summarize our prioritization process is that we prioritize in descending over:
* What people are willing to spend their free time to build (I can't order other people to build high-priority bindings and my own free time is already accounted for by improving Haskell bindings that power a lot of shared tooling such as the language server)
* Bindings specific to DevOps use cases (e.g. Go / Python / Ruby / Nix / JSON / YAML), since they are the dominant languages and formats in this space)
* Bindings that can be used to create derived bindings (e.g. Rust, which can then be used to create a binding in any language that can bind to C. In fact, this is how the upcoming Python bindings work. See: https://pypi.org/project/dhall/)
* Bindings that users request (We have a yearly survey where we ask users to inform the direction of the ecosystem. Python was the most requested language in the most recent survey)
* Overall language popularity (as the final tiebreaker)
So I hope this illustrates that there is a lot more that goes into these decisions beyond just which language is the most popular and we're not being obtuse or dilettantes just because we haven't gotten to a specific language, yet.
Also, any language without an official browser JavaScript implementation obviously wasn't created to solve the same problems I'm interested in.
This has many obvious benefits, including testing!
A key aspect is for the owner of the "config system" (the stuff that takes the output of each config program and applies it) to standardize the API for config generators. Inputs, outputs, runtime env, etc. Then let teams integrate with that API however they want.
Those configs would be unmanageable. You would no longer be able to set a value, interfaces like `git config` or your-favorite-preferences-GUI-pane wouldn't be possible. And, generally speaking, I believe it's a bad and non-user-friendly thing.
However, it makes perfect sense to use a programming language, if your program is programmable and is meant to be programmable. Like a shell's rc files.
It's also probably okay to use programs as configs, if everything related to the program is built decoratively, so all the configuration files are generated (from some higher-order configuration) and not meant to be ever manipulated, only replaced with the newly generated versions. Programmatically generating programs is simple, programmatically manipulating programs is not.
It is also fine if you explicitly want to restrict an ability to manipulate the configuration or create management interfaces, requiring human programming (whenever the tool is meant to be programmable or not). Feels like a weird idea, but I can see this being considered as a trade-off.
I went from trying to embed python in a C application to embedding TCL and was blown away how easy it was, and that it supported threads. this was back in 2003 I remember trying it with lua, but the parallelism story wasn't really great until at least 5.1. TCL did it right from 8.4 and things hasn't really changed since, except that Lua catched up.
Django seems to work ok with Python configuration. I haven't seen found much logic happen in the projects I've used. But the user is the developer not an end-user. So security is not an issue from this angle.
I'm of the opinion that a simple config format like .ini is best for the end-user of a small application, while a validated schema (with code if needed) is best for a large one. To that end I'm currently experimenting with tconf to bring together those approaches under one package: https://github.com/mixmastamyk/tconf
This is indeed a problem, but something tells me the solution isn't "have them use Python."
* Human-readable and writable, including on systems where you have only very basic text editing tools * Easy to template (for example mass deployments via Ansible or other such tool) * Easy to use with grep/sed/etc
The format I've found that is easiest to work with is what sysctl and OpenWRT use for their configs. You can have complex hierarchies, but every line stands on its own. This means you don't have to be careful about where in the config a particular line is, if it is in the proper block, etc. Also getting information on a particular sub-item is as easy as running a single grep command.
But the best way to sell it to people who don't understand or who aren't yet on board is to name it properly, which the article fails to do. What this is describing is no longer configuration. This is now a modular plugin architecture that only in the most basic usage case implements a configuration interface.
I must admit that's where I stopped taking it seriously. If you need helper functions and temp variables, you do need a good Turing-complete language, but we're no longer talking about config files. Maybe about config system, but not a config file. And confusing the two means missing the point why config files exist at all. I mean not everybody has to buy into code vs. data division, but if you miss entirely why it exists maybe it not thought through enough.
Consider, if possible, the chance that if your program needs a subprogram for configuring itself, then maybe a bootstrap event hook or some tap into the bootstrap process like a plugin or something is a better fit for what you are trying to do. Actual managed extension points instead of ad-hoc ones will make your program much more useful in the long run.
Anything that's changing from one run of your application to another is config. Static settings that don’t change from one run to another are not config, they are just static pieces of data and it does not matter how you store them other than that you ensure it meets the operating need of your application.
Configs, however, which can change from one run to the next, are different. They need to be factored out of code completely and only addressed by inspection of the runtime environment.
Whatever tool you use to ensure they are injected to the environment is also a totally uninteresting decision as long as it meets the operating needs of your deployment system.
That’s it.
1. store static constants and data items however you want as long as your program meets its operating requirements
2. config is not the same as static constants or static data, config can change from one run of the application to another.
3. factor config completely out of the code so it is solely referenced as part of the environment
4. store external configs however you want so long as the operating constraints of the deployment system are met.
Within items 1 and 4, debate over relative merits of different tools is almost always useless bikeshedding unless it boils down to a real operating constraint of either the app itself or the deployment system.
For example, a system that puts secret access tokens (config, not static settings) into the environment by storing them in plaintext environment variables might violate a security operating constraint of the deploy system and so a different system that manages encrypted secrets injected into the environment safely could be the winner for real operating reasons.
Meanwhile, whether to store “staging” vs “production” database connections in Python / YAML / JSON / TOML / etc. because of comments / whitespace / use of builtin library “extra” code / whatever is just pure bikeshedding waste of time.
More importantly this helps us have tight validation around configuration ecosystem that defined experimentation and server side overrides. See config delta blog post: https://medium.com/crunchyroll/introducing-crunchyroll-confi...
Of course, you start out just cargo culting emacs config, so many may not even realize emacs is configured via code. You can also get pretty far with setq functions, essentially just assigning values to variables.
But learning to grok and code elisp is when you start to really see emacs’ power.
The advantage of declarative configuration is that it provides a sync barrier to the human and a safe entry-point. I imagine in a pure language without global runtime state you could use this method but in the more mainstream languages it is likely to trip you up. I will refrain.
I know the majority of developers hate XML config files, yet...
I try to make my apps depend on config files and manifests as little as possible, but DSL vs fully featured scripting languages has always been a big conundrum in software development. Is python a solution? the problem is that at some point you might be tempted to add json/yaml config files to your python config scripts...
Declarative languages are a work around for bad APIs.
I usually use libconfig for configuration, which is better than json and yaml and has implementations in many languages (although I'm not aware of any javascript implementation).
Having the entire python language (specifically the ability to inspect the local system) available BEFORE you declare your package (as a side effect!) means you cannot reliably learn anything about a package without having an entire operating system with python and all package dependencies pre-installed. It can lead to scenarios where you cannot automatically query a packages dependencies without having those dependencies already pre-installed. Or when installing packages in different order yields totally different results.
On the data <-> code spectrum, configuration should be closer to data than to code, perhaps a pure function that takes a high level config to a lower level config.
Replace "configuration" with "scripting layer", and you'll have a much better perspective on "config" "vs" "code". Especially if your "code" is also a scripting language or open source.
I'm guessing that this is in place of just importing the config module, because of this line:
> You can even import the very package you're configuring.
Which sounds like a recipe for circular import disaster.
What's "a bit fucked up" is expecting the configuration file to be able to support this when this can and should be done by the application itself. If you feel the need to do these things in the configuration file itself, then the application is providing insufficient abstractions.
Like sure, there's definitely use in an application dynamically loading its own potentially-user-supplied code to modify its behavior, but most people don't call that code "configuration files"; we call that code "extensions" or "modules" or "plugins" or "scripts" or somesuch.
Yaml is the poster boy for easy but not simply.
A programming language can’t do that (or if it could via eval, it would be a bad idea)
The other 10% of the time where you need some level of logic to modify some setting value, there's no substitute for something that people are already familiar with. Just let people have their 3 line js function instead of a 2-3 other dependencies that add more convoluted logic to the build system.
Trying to create a config specific language with more advanced features/logic is an absolute mess where you end up learning a second language, people are unhappy with how certain features aren't implemented, and others are unhappy with how too many features exist, and it's just all round chaos.
tl;dr There should be no inbetween: Either you have a simple key/value config file that someone can learn in 5 minutes, or you let people use a language they're already familiar with.
Case in point, PHP's "Smarty" templating engine. Because lord forbid people learn to use PHP as a templating language (which is ALREADY IS), when instead they can learn to use an ad hoc, informally-specified, bug-ridden, slow implementation of half of PHP, which is implemented in PHP, and dynamically generates PHP code.
https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule
>Greenspun's tenth rule of programming is an aphorism in computer programming and especially programming language circles that states:
>Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.
Your templating engine sucks and everything you have ever written is spaghetti code (yes, you)
http://www.workingsoftware.com.au/page/Your_templating_engin...
>This is where folks will start to get technical about why their code isn't spaghetti code.
>Oh no! Not me because I use (Django|Rails|Haskell|Node.js|Symfony|CakePHP|Tornado|Plone) and the superior templating language (Django Template|ERB|Mustache|PHP|TAL|Smarty) doesn't allow business logic in templates and enforces a separation of concerns
>-You, the reader, December 2011
>The problem is that in every single one of these circumstances, the templating engine has 2 primary responsibilities:
>The fragmentification and inclusion of common interface assets (to avoid duplication and maintaining the same code in multiple files) - eg. headers, footers, sidebars and the like
>The output of dynamic content
>The output of dynamic content requires logic (if/else, loops being about the bare minimum).
>The creators of templating languages have, to varying degress, availed themselves of a false dichotomy, namely that there exists "presentation" logic and "business" logic, the former of which belongs in templates and the latter of which has absolutely no place in templates and must be banished immediately.
>I say false dichotomy because this is an arbitrary separation - a choice made by the person who created the templating engine and is completely subjective.
Yet another reason smarty sucks
https://www.sitepoint.com/community/t/yet-another-reason-sma...
>Which brings me right back to my chief criticism of smarty which to this day it’s adherents can’t answer – what the Hell is the point? [...]
>I hated smarty from the day I tried it and I was always wondering why would anyone use it since you’re forced to learn some weird syntax that’s so similar to php - so why not use php from the start?
https://news.ycombinator.com/item?id=4605887
>PHP is itself a template engine. Bolting a template engine on top of that makes no sense whatsoever.
https://news.ycombinator.com/item?id=20736493
>+1 for Turing complete programming languages instead of half-assed config languages.
https://news.ycombinator.com/item?id=20735231
>I was suspicious of YAML from day one, when they announced "Yet Another Markup Language (YAML) 1.0", because it obviously WASN'T a markup language. Who did they think they were fooling?
There's examples of this working effectively: millions of people can use excel to perform computations on groups of cells - the formulae they're entering include basic programming constructs like assignment, selection, loops, etc - yet it's not presented as a "programming system", which otherwise seems to put people off.
I recall reading an anecdote about the TECO editor (or some other emacs predecessor). Where the editor was being used by secretaries and other non-programmers, but they'd have no problems configuring it with the documentation they had available - they were never told that they were actually programming when they were doing so.
Perhaps it's a bit much to give the user a full-blown programming language for their configuration and expect them to have no problems, but it seems like limited programming concepts can be learned by just about anyone if presented in the right way.
And as you said, presentation is key. Presented as "use these tools to set this up" things could go well. Presented as "write this in python" it may not go at all.
Then goes on to recommend python. What?
As this is provably impossible, this "solution" introduces exactly one of the problems the author was trying to avoid:
> can't be validated
That's why 'conservative'. I.e. it's allowed to reject a valid, terminating program, but if it does pass the check, your program is guaranteed to terminate. This is something that's possible, the only question is the tradeoff between the subset of the syntax and how complicated is the static analysis.
- Clang (depending on warnings level) may reject a valid program -- doesn't make it less useful, you just suppress the check for the offending line and carry on
- Rust borrow checker may be seemingly picky and reject a perfectly valid program form your viewpoint. Does it make it less useful? I wouldn't say so.
My point is that the validator can't give you the safety property that is claimed as a defence against one of the inherent issues with this approach.
I mostly have a non-malicious user in mind though (i.e. end-user software, where the software and the config have same permissions)
If you do have such security concerns, you probably need a sandbox at some point. E.g. big source of my frustration are CI pipelines -- they run isolated anyway and execute arbitrary code. Having a YAML there does nothing for the security.
For example if you set a path to Stack as a script shebang, you can write interpreted-mode shell scripts in Haskell.
You could also use JSON or environment vars...
Troll aside, nix is mentioned and IMHO is the perfect tool for the task, but the author throws it away in few lines because it’s an “overkill”.
If it is truly needed, use/write a config generator instead. We have so many tools in the build chain now that config generation can be easily automated into the deployment process.
See m4, BPF, PDF, etc...
I haven't experienced cases where I've needed Turing-complete configuration, and regard Turing-complete configuration as a code smell. However, people at least feel they sometimes need Turing-completeness. When they do, I think we can all agree that ideally they'd not need to add another language to their project in order to do so.
I think the ideal case would be a declarative Turing-incomplete configuration language with an escape hatch to a Turing-complete superset, similar to Rust's unsafe blocks. Bonus points for forcing impurity to be similarly confined without scaring people with monads.
If done well, with both an AoT compiler generating highly optimized native code and an interpreter for loading configuration, one might even be able to bring the "configuration should be Turing-incomplete" and "configuration should be in the same language as the application" folks under the same big tent. In an ideal world, the AoT compiler could optionally partially evaluate your program with respect to a configuration file, basically conditional compilation on steroids, with guarantees that the conditional compilation hasn't altered program behavior.
Static analysis would be easier, both for developer tools and the compiler's optimizer. A quick code search would help you focus on the gnarly bits of code. A code reviewer would take pause before approving some code with an "unsafe impure loopy {}" block (assuming "loopy" is the keyword for the Turing-complete superset).