I wrote this code, and it made me super happy!
data Variety = Installer | Target
deriving (Eq)
seed :: UserInput -> Versioned Variety Host
seed userinput ver = host "foo"
& ver ( (== Installer) --> hostname "installer"
<|> (== Target) --> hostname (inputHostname userinput)
)
& osDebian Unstable X86_64
& Apt.stdSourcesList
& Apt.installed ["linux-image-amd64"]
& Grub.installed PC
& XFCE.installed
& ver ( (== Installer) --> desktopUser defaultUser
<|> (== Target) --> desktopUser (inputUsername userinput)
)
& ver ( (== Installer) --> autostartInstaller )
This is doing so much in so little space and with so little fuss! It's
completely defining two different versions of a Host
. One version is the
Installer
, which in turn installs the Target
. The code above provides
all the information that propellor needs to convert a copy of
the Installer
into the Target
, which it can do very efficiently. For
example, it knows that the default user account should be deleted, and a
new user account created based on the user's input of their name.
The germ of this idea comes from a short presentation I made about
propellor in Portland several years ago. I was describing
RevertableProperty
, and Joachim Breitner pointed out that to use it, the
user essentially has to keep track of the evolution of their Host
in
their head. It would be better for propellor to know what past versions
looked like, so it can know when a RevertableProperty
needs to be
reverted.
I didn't see a way to address the objection for years. I was hung up
on the problem that propellor's properties can't be compared for equality,
because functions can't be compared for equality (generally). And on the
problem that it would be hard for propellor to pull old versions of a Host
out of git. But then I ran into the situation where I needed these two
closely related hosts to be defined in a single file, and it all fell
into place.
The basic idea is that propellor first reverts all the revertible properties for other versions. Then it ensures the property for the current version.
Another use for it would be if you wanted to be able to roll back changes to a Host. For example:
foos :: Versioned Int Host
foos ver = host "foo"
& hostname "foo.example.com"
& ver ( (== 1) --> Apache.modEnabled "mpm_worker"
<|> (>= 2) --> Apache.modEnabled "mpm_event"
)
& ver ( (>= 3) --> Apt.unattendedUpgrades )
foo :: Host
foo = foos `version` (4 :: Int)
Versioned properties can also be defined:
foobar :: Versioned Int -> RevertableProperty DebianLike DebianLike
foobar ver =
ver ( (== 1) --> (Apt.installed "foo" <!> Apt.removed "foo")
<|> (== 2) --> (Apt.installed "bar" <!> Apt.removed "bar")
)
Notice that I've embedded a small DSL for versioning into the propellor config file syntax. While implementing versioning took all day, that part was super easy; Haskell config files win again!
API documentation for this feature
PS: Not really FRP, probably. But time-varying in a FRP-like way.
Development of this was sponsored by Jake Vosloo on Patreon.