mirror of
https://github.com/rwxrob/dot
synced 2024-11-14 18:12:56 +00:00
398 lines
11 KiB
Perl
Executable File
398 lines
11 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
# This is an unoptimized prototype with lots of subshells in preparation
|
|
# for port to Go eventually. In other words, this just works for now.
|
|
# See documentation after __END__ at the bottom. (I didn't feel like
|
|
# writing it in Perl POD format, sorry.)
|
|
|
|
use v5.14;
|
|
use File::Path qw(make_path);
|
|
no strict;
|
|
|
|
for (qw( yq docker id )) {
|
|
not `which $_` and say "Missing dependency: $_" and exit 1;
|
|
}
|
|
|
|
# attemp to filter out that piece of shit Python yq
|
|
`yq --help` =~ /eval-all.*shell-completion/s or say "Missing GOOD yq." and exit 1;
|
|
|
|
my $wsdir = $ENV{'WORKSPACES'} || "$ENV{'HOME'}/Workspaces";
|
|
my $image = image() || $ENV{'WORKSPACE_IMAGE'} || 'rwxrob/workspace';
|
|
my $editor = $ENV{'EDITOR'} || 'vi';
|
|
|
|
not -d $wsdir and mkdir($wsdir);
|
|
|
|
sub end {
|
|
say shift;
|
|
exit;
|
|
}
|
|
|
|
sub get_ws_list {
|
|
opendir( my $dh, $wsdir )
|
|
|| end "Failed to open workspace directory: $!";
|
|
return grep !/^(\.\.?|README)/, readdir($dh);
|
|
}
|
|
|
|
sub image {
|
|
not -e "$wsdir/.config.yml" and return "";
|
|
my $image = `yq e '.image' "$wsdir/.config.yml"`;
|
|
chomp $image;
|
|
return $image;
|
|
}
|
|
|
|
sub image_exists {
|
|
my $name = shift;
|
|
`docker image inspect $name >/dev/null 2>&1`;
|
|
return $? == 0;
|
|
}
|
|
|
|
sub x_image {
|
|
my $name = shift;
|
|
my $conf = "$wsdir/.config.yml";
|
|
not $name and say "$image" and return;
|
|
not image_exists($name) and say "Image '$name' not found. Pull." and return;
|
|
not -e $conf and `yq e -n '.image="$name"' >| "$conf"` and return;
|
|
`yq e -i '.image="$name"' "$conf"` and return;
|
|
}
|
|
|
|
sub x_pull {
|
|
my $arg = shift;
|
|
$arg and exec qq{docker pull $arg};
|
|
exec qq{docker pull $image};
|
|
}
|
|
|
|
|
|
sub x_config {
|
|
my $name = shift || x_pick();
|
|
say "$wsdir/$name/.config/ws/config.yml";
|
|
}
|
|
|
|
sub x_edit {
|
|
my $name = shift || x_pick();
|
|
my $conf = "$wsdir/$name/.config/ws/config.yml";
|
|
exec $editor, $conf;
|
|
}
|
|
|
|
sub expand {
|
|
my $r = shift;
|
|
return unless @$r;
|
|
map { !/:/; $_ = "$_:$_" } @$r;
|
|
}
|
|
|
|
sub x_open {
|
|
my @names = get_ws_list();
|
|
not @names and return x_create(@_);
|
|
|
|
my $name = shift || x_pick();
|
|
|
|
my $dir = "$wsdir/$name";
|
|
not -d $dir and return x_create($name);
|
|
chdir $dir;
|
|
|
|
my $confpath=`ws config $name`;
|
|
chomp $confpath;
|
|
not $confpath and end "No config file found.";
|
|
|
|
my $username = `yq e .user.name "$confpath"`;
|
|
chomp $username;
|
|
not $username and end "No user name found.";
|
|
|
|
my @cmd = (qw(docker run -it --rm --privileged --name), $name, '-h', $name );
|
|
|
|
my @ports = qx(yq e '.ports[]' `ws config $name`);
|
|
chomp @ports;
|
|
if ($ports[0] eq 'all') {
|
|
@cmd = ( @cmd, '--network', 'host' );
|
|
} else {
|
|
expand \@ports;
|
|
map { @cmd = ( @cmd, '-p', $_ ) } @ports;
|
|
}
|
|
|
|
my @mounts = qx(yq e '.mounts[]' `ws config $name`);
|
|
chomp @mounts;
|
|
unshift @mounts, "/var/run/docker.sock";
|
|
expand \@mounts;
|
|
unshift @mounts, "$dir:/home/$username";
|
|
map { @cmd = ( @cmd, '-v', $_ ) } @mounts;
|
|
|
|
#say "Executing: " . join( " ", ( @cmd, $image ) );
|
|
exec( @cmd, $image );
|
|
}
|
|
|
|
sub x_list {
|
|
map { say $_} get_ws_list;
|
|
}
|
|
*x_ls = *x_list;
|
|
|
|
sub x_remove {
|
|
my $name = shift;
|
|
$name or $name = x_pick();
|
|
my $path = "$wsdir/$name";
|
|
! -d $path and say "Path not found: $path" and return;
|
|
print "Sure you want to remove '$path'? (n) ";
|
|
<STDIN> =~ /^y/i or return;
|
|
-d $path and `rm -rf $path`;
|
|
say "Removed $wsdir/$name directory.";
|
|
}
|
|
*x_rm = *x_remove;
|
|
|
|
sub x_pick {
|
|
my $n = 1;
|
|
my @names = get_ws_list();
|
|
not @names and end "No workspaces found.";
|
|
my @list = get_ws_list();
|
|
scalar(@list) == 1 and return $list[0];
|
|
map { say "$n. $_"; $n++ } @list;
|
|
my $pick;
|
|
until ( 0 < $pick && $pick < $n ) {
|
|
print "#? ";
|
|
$pick = <STDIN>;
|
|
}
|
|
my $name = @names[ $pick - 1 ];
|
|
return $name;
|
|
}
|
|
|
|
sub x_usage {
|
|
my @path = split( /\//, $0 );
|
|
my $exe = pop @path;
|
|
say "usage: $exe [COMMAND]";
|
|
}
|
|
|
|
sub x_create {
|
|
my $name, $dir, $username, $userid, $groupname, $groupid,
|
|
$ports, $mounts, $getlatest, $resp, $confd, $conf;
|
|
|
|
$name = shift;
|
|
$dir = "$wsdir/$name";
|
|
|
|
until ($name) {
|
|
print "Workspace Name: ";
|
|
$name = <STDIN>;
|
|
chomp $name;
|
|
not $name and next;
|
|
$dir = "$wsdir/$name";
|
|
if ( -d $dir ) {
|
|
say "Workspace ($name) already exists.";
|
|
$name = "";
|
|
next;
|
|
}
|
|
}
|
|
|
|
$username = `id -un`;
|
|
chomp $username;
|
|
print "User Name ($username): ";
|
|
$resp = <STDIN>;
|
|
chomp $resp;
|
|
$resp and $username = $resp;
|
|
$userid = `id -u`;
|
|
chomp $userid;
|
|
say "User ID will be $userid.";
|
|
|
|
$shell = $ENV{'SHELL'};
|
|
print "User Shell ($shell): ";
|
|
$resp = <STDIN>;
|
|
chomp $resp;
|
|
$resp and $shell = $resp;
|
|
|
|
$groupname = `id -gn`;
|
|
chomp $groupname;
|
|
print "Group Name ($groupname): ";
|
|
$resp = <STDIN>;
|
|
chomp $resp;
|
|
$resp and $groupname = $resp;
|
|
$groupid = `id -g`;
|
|
chomp $groupid;
|
|
say "Group ID will be $groupid.";
|
|
|
|
$ports = "";
|
|
printf "Ports: ";
|
|
$resp = <STDIN>;
|
|
chomp $resp;
|
|
$ports = $resp;
|
|
|
|
$mounts = '';
|
|
printf "Extra Mounts: ";
|
|
$resp = <STDIN>;
|
|
chomp $resp;
|
|
$mounts = $resp;
|
|
|
|
$confd = "$dir/.config/ws/";
|
|
$conf = "$confd/config.yml";
|
|
-d $dir and end 'Workspace directory already exists: ' . $dir;
|
|
make_path($dir)
|
|
|| end 'Failed to create workspace directory: ' . $dir;
|
|
make_path($confd)
|
|
|| end 'Failed to create config directory: ' . $confd;
|
|
open( my $fh, ">", $conf )
|
|
|| end 'Failed to open config file for writing: ' . $conf;
|
|
|
|
say $fh '# This file is used by the ws workspace container manager.';
|
|
say $fh "# It's fine to make changes directly and is shared by host.";
|
|
say $fh "\nname: $name";
|
|
|
|
say $fh "\nuser:\n name: $username\n id: $userid\n shell: $shell";
|
|
say $fh "\ngroup:\n name: $groupname\n id: $groupid";
|
|
say $fh "";
|
|
|
|
if ($ports) {
|
|
$ports =~ s/ +/,/g;
|
|
say $fh "ports: [$ports]";
|
|
}
|
|
if ($mounts) {
|
|
$mounts = s/ +/,/g;
|
|
say $fh "ports: [$ports]";
|
|
}
|
|
close $fh;
|
|
|
|
x_open($name);
|
|
|
|
}
|
|
|
|
sub x_dir {
|
|
my $name = shift || x_pick();
|
|
say "$wsdir/$name";
|
|
}
|
|
*x_d = *x_dir;
|
|
|
|
# ------------------------ bash tab completion -----------------------
|
|
# `complete -C ws ws` -> ~/.bashrc
|
|
# (any command starting with x_)
|
|
|
|
if ( $ENV{'COMP_LINE'} ) {
|
|
@completions = grep s/^x_//, keys %{"main::"};
|
|
@completions = ( @completions, get_ws_list );
|
|
if ( not $ARGV[1] ) {
|
|
map { say $_} @completions;
|
|
exit;
|
|
}
|
|
map { /^$ARGV[1]/ && say $_} @completions;
|
|
exit;
|
|
}
|
|
|
|
# ------------------------------- main -------------------------------
|
|
|
|
if ( not "$ARGV[0]" ) {
|
|
ws_get_list and x_open and exit;
|
|
wx_usage and exit;
|
|
}
|
|
|
|
my $first = shift @ARGV;
|
|
|
|
$cname = "x_$first";
|
|
if (my $func = main->can($cname)) {
|
|
&$func(@ARGV);
|
|
exit;
|
|
}
|
|
|
|
x_open($first);
|
|
|
|
__END__
|
|
|
|
Workspace Container Management Utility
|
|
|
|
The ws container workspace management utility provides what most
|
|
engineers, developers, and learners want from a workspace container
|
|
manager with reasonable defaults: a single consistent bind-mount to home
|
|
directory, a few configurable volume and port bindings, and --- most
|
|
importantly --- to be able to *quickly* switch between them and keep
|
|
them up to date with the latest workspace image as new tools are added
|
|
and old ones improved, even use the same workspace with different
|
|
images.
|
|
|
|
Command: pull
|
|
|
|
Pulls the latest current image (see image) if set, otherwise pulls the named image from registry.
|
|
|
|
Command: create
|
|
|
|
Creates a directory for each workspace in the parent directory indicated
|
|
by the WORKSPACES environment variable (default ~/Workspaces) and
|
|
automatically bind-mounts it as the home directory for the workspace
|
|
user. The `.config/ws/config.yml` file will be created within it and
|
|
used by subsequent open commands (which activate the workspace).
|
|
A `/var/run/synced` file will also be created after the
|
|
/usr/share/workspace directory has first copied for the user home
|
|
directory (much like /etc/skel). Removing it later will cause the
|
|
ephemeral workspace image entrypoint to do another sync with whatever
|
|
the current local image is replacing everything that it contains with
|
|
copies
|
|
|
|
During creation the user will be prompted for the following (defaults
|
|
taken from the current user):
|
|
|
|
* Workspace Name - name of workspace and directory in $WORKSPACES
|
|
* User Name - user name for workspace container
|
|
* User Shell - user shell for workspace container
|
|
* Group Name - user group name for workspace container
|
|
* Ports - ports (ex: 80 -> -p 80:80)
|
|
* Extra Mounts - extra bind-mounts (ex: /tmp -> -v /tmp:/tmp)
|
|
|
|
The directory within $WORKSPACES is always mounted the user home
|
|
directory. (Hence, "extra mounts")
|
|
|
|
The user and group ID will always match that of the current user. This
|
|
is sufficient for most work situations. When different IDs are required,
|
|
first create a user on the local host system that has the wanted IDs and
|
|
change to that user before running ws. This prevents file owner and
|
|
group ID problems later. Note that this is a constraint of working with
|
|
containers in docker itself, not this utility.
|
|
|
|
The hostname of the container is automatically set to the name of the
|
|
workspace.
|
|
|
|
Ports and mounts can be specified in shorthand as single values
|
|
without the colon (as well as the normal colon syntax). Separate
|
|
multiple values with spaces. No other validation of port input
|
|
values is done allowing any syntax that is valid for the docker -v
|
|
switch to be passed.
|
|
|
|
Note that it is generally best to just use a single home directory and
|
|
create symbolic and/or hardlinks within the container to stuff within
|
|
that home directory. This centralizes the workspace directory for
|
|
convenience and provides a quick visual representation of what is
|
|
included in the work.
|
|
|
|
The /var/run/docker.sock socket is always mounted. Unfortunately, the
|
|
owner user id or group id must exactly match that of the workspace.
|
|
A warning is printed if these do not coincide since usually the host
|
|
docker instance is wanted and not another contained within the
|
|
workspace, but not always.
|
|
|
|
Command: config
|
|
|
|
Prints the path to the configuration file for the given workspace.
|
|
|
|
Command: edit
|
|
|
|
Opens the configuration file (see config) for editing.
|
|
|
|
Command: open
|
|
|
|
Opens the named workspace (or creates if not found). This is the default
|
|
if no known command is passed as the first argument in which case the
|
|
first argument it assumed to be the name of the workspace to switch to.
|
|
|
|
Command: rm, remove
|
|
|
|
Remove the specified workspace with an interactive confirmation
|
|
prompt. Will delete the directory and named container.
|
|
|
|
Command: ls, list
|
|
|
|
List all workspaces (lexigraphically from WORKSPACES directory).
|
|
|
|
Command: image
|
|
|
|
Sets the default image to be used. By default it is rwxrob/workspace but
|
|
it can be set to any image name that has been pulled into the current
|
|
Docker installation.
|
|
|
|
Bash Tab Completion
|
|
|
|
This script supports its own completion. Simply add the following to
|
|
your .bashrc (or other) startup configuration file:
|
|
|
|
complete -C ws ws
|
|
|
|
Note that this assumes the ws command is included in the PATH.
|