-
-
Save lf94/d16b55fa7c79d989e211367a44d1a9cc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(*------------------------------------------------------------------------------ | |
REPLICARE | |
recipere + deplicare | |
"to receive" + "explain" | |
Write SML programs to output shell code for installations and deployments. | |
Modify as needed. | |
-------------------------------------------------------------------------------- | |
Why not just use shell scripts directly? | |
The lack of a type-system in all shell scripting languages is, when you think | |
about, horrific. It takes one bad input to destroy a system. With types this can | |
lift the level of safety very high, as now the script will not run before it is | |
considered correct. In hindsight it will be clear that all scripting languages | |
should have had strong typing from the beginning. | |
There is not much more to say than that. Hindley-Milner type systems allow a | |
great level of type inference, which means the developer of scripts does not | |
need to type everything. It's this gradient of typing which suits a whole | |
spectrum of needs that is powerful. | |
-------------------------------------------------------------------------------- | |
Utilities *) | |
val join = String.concatWith; | |
fun cp (programInvocation:string) (srcs:string list) (dest:string) | |
= join " " [programInvocation, join " " srcs, dest] | |
; | |
datatype State = Exists | Fresh | |
datatype OSObject = User of string | Directory of string | Software of string; | |
datatype Location | |
= Local of string list | |
| Remote of string list | |
; | |
(* Change this to a prefered copy program, i.e. scp *) | |
fun rsync (port:string) = cp (String.concat ["rsync -vv -e 'ssh -p ", port, "'"]); | |
(* Change this to the package manager on the remote system, i.e. pacman *) | |
structure PackageManager = | |
struct | |
val pkgman = "apt" | |
fun install software = String.concat [pkgman, " install -y ", software]; | |
end | |
fun createSession (user, host, port) | |
= let | |
fun remote cmd | |
= String.concat ["ssh -p ", port, " ", user, "@", host, " -C '", cmd, "'"] | |
; | |
fun copy (Local from) (Remote to) | |
= rsync port from ("'" ^ user ^ "@" ^ host ^ ":" ^ (join " " to) ^ "'") | |
| copy (Remote from) (Local to) | |
= rsync port ["'" ^ user ^ "@" ^ host ^ ":" ^ (join " " from) ^ "'"] (List.nth (to, 0)) | |
| copy _ _ | |
= "" | |
; | |
fun ensure (User user) Exists | |
= (remote o String.concat) ["id ", user, " || useradd -m ", user] | |
| ensure (Directory dir) Exists | |
= (remote o String.concat) ["[ -d \"", dir, "\" ] || mkdir -p ", dir] | |
| ensure (Directory dir) Fresh | |
= (remote o String.concat) ["rm -rf ", dir, " && mkdir -p ", dir] | |
| ensure (Software ware) Exists | |
= (remote o String.concat) ["which ", ware, " || ", PackageManager.install ware] | |
| ensure (Software ware) Fresh | |
= (remote o String.concat) [PackageManager.install ware] | |
| ensure _ _ | |
= "" | |
; | |
in (copy, ensure, remote) | |
end; | |
fun each statements var path = | |
String.concat [ | |
String.concat ["for ", var, " in ", path, "; do "], | |
join "; " statements, | |
"; done" | |
]; | |
fun shell cmd = cmd; | |
fun exec cmds = print (join " && " cmds); | |
(*------------------------------------------------------------------------------ | |
Example: deploy blog to a host *) | |
val target = ("root", "len.falken.directory", "9999"); | |
val (copy, ensure, remote) = createSession target; | |
exec [ | |
ensure (User "len") Exists, | |
ensure (Directory "/home/len/www/") Exists, | |
ensure (Software "rsync") Exists, | |
shell "/home/len/Wiki/rss.sh > /home/len/Wiki/feed.xml", | |
copy (Local ["/home/len/Wiki/public", "/home/len/Wiki/feed.xml", "/home/len/Wiki/index.css"]) | |
(Remote ["/home/len/www/"]) | |
] | |
(*------------------------------------------------------------------------------ | |
Example: self-host git projects *) | |
val cwd = "/home/len/Code/len/git-self-host"; | |
exec [ | |
ensure (User "len") Exists, | |
ensure (Software "rsync") Exists, | |
ensure (Software "git") Exists, | |
shell String.concat [cwd, "/rss.sh > ", cwd, "/code.xml"], | |
copy (Local [cwd ^ "/code.xml", cwd ^ "/index.css"]) | |
(Remote ["/home/len/www/"]), | |
each [ | |
copy (Local ["$file/.git/"]) | |
(Remote ["/home/len/www/$(basename $file).git"]), | |
remote "cd /home/len/www/$(basename $file).git/ && git config --bool core.bare true" | |
] "file" (cwd ^ "/../*") | |
] | |
(* | |
TODO: Use configuration templates for things like nginx | |
This will be simple: copy (Local [template "templates/nginx.conf" [["key","value"]]]) (Remote ["some place"]) | |
template will return a path to a temporary file | |
It will invoke whatever templating tech you'd like. | |
*) |
Hm, I'm not sure this example is useful to bring up, or anything that requires to "know" your targets... Let's say that was a typo - this could in fact be encoded in the program to be prevented. The idea I'm trying to convey here is "type as much as you need, abstract as much as you need". Standard ML doesn't support value literal types like TypeScript so it can't be done as elegantly at the type system level, but if you really wanted to, you could do:
datatype Username = Len | Lee | Leo | Lem;
ensure (User Len) Exists;
And then you can expand those to strings throughout the code...!
There are a few alternatives also I can think of, but you get the idea.
FWIW TypeScript would work just as well. I prefer Standard ML these days for personal code: http://len.falken.directory/p-lang/100-year-programs.txt
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Interesting idea. This reminded me of the discomfort caused by my use of Ansible and got the grey matter tingling.
Interestingly, your revision history demonstrates the limited impact of applying types to this problem.
Both versions type check; only one is correct. Until you can type the behaviour of the outside world, this has limited utility (and with this incantation, a Nix maximalist is summoned forth from the digital ether).
Thank you for sharing!