forked from tomeichlersmith/denv
-
Notifications
You must be signed in to change notification settings - Fork 0
/
denv
executable file
·1318 lines (1241 loc) · 43.8 KB
/
denv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
#
# This file is part of the denv project: https://github.com/tomeichlersmith/denv
#
# Copyright (C) 2021 denv contributors
#
# denv is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3
# as published by the Free Software Foundation.
#
# denv is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with denv; if not, see <http://www.gnu.org/licenses/>.
denv_version=1.1.1
# POSIX
set -o errexit
set -o nounset
# deduce full path to our executable so we can locate the entrypoint executable
# we need to write our own function to deduce the full path to denv
# because MacOS does not have the 'realpath' function out of the box.
denv_install_dir="$(dirname "$0")"
denv_install_dir="$(cd "${denv_install_dir}" && pwd -P)"
denv_fullpath="${denv_install_dir}/denv"
# deduce full path to entrypoint executable
denv_entrypoint="$(dirname "${denv_fullpath}" || true)/_denv_entrypoint"
if [ -n "${DENV_DEBUG+x}" ]; then
set -o xtrace
fi
if [ -n "${DENV_INFO+x}" ]; then
_denv_info() {
printf "\033[32;1m INFO: \033[0m\033[32m%s\n" "$1"
shift
while [ "$#" -gt "0" ]; do
printf ' %s\n' "$1"
shift
done
printf "\033[0m"
}
else
_denv_info() {
:
}
fi
# print each argument on its own line with the first line
# prefixed with "ERROR: ".
_denv_error() {
printf >&2 "\033[1;31mERROR: \033[0m\033[31m%s\n" "$1"
shift
while [ "$#" -gt "0" ]; do
printf >&2 " %s\n" "$1"
shift
done
printf >&2 "\033[0m"
}
###################################################################################################
# helper functions that can be shared across runners
###################################################################################################
# convert the ${denv_image} variable into a filename stub
# Arguments
# denv_image environment variable
# Outputs
# a simple filename with special characters replaced for
# easier interaction with the filesystem
_denv_image_to_filename() {
# convert slashes to underscores and colons to hyphens
printf '%s' "${denv_image}" | tr '/' '_' | tr ':' '-'
}
# convert the space-separate mounts array into the space-separate
# list of arguments to provide to run command
# Arguments
# 1 - option name to prefix each mount with
# 2 - optional mount setting to append to the mount
# denv_mounts - env var holding space-separated list of mounts
# Outputs
# the list of mount-related arguments to provide to run command
_denv_mounts_to_args() {
mount_args=""
for mount in ${denv_mounts}; do
mount_args="${1} ${mount}:${mount}${2:+:${2}} ${mount_args}"
done
echo "${mount_args}"
}
# convert the environment variable related configuration parameters
# to a string list of arguments for the container runner
# Arguments
# 1 - option name to prefix each environment variable with
# denv_environment - space-separated key=value pairs to put into the denv
# Outputs
# list of env-var-related arugments to provide to run command
_denv_environment_to_args() {
env_args=""
for ev in ${denv_environment}; do
env_args="${1} ${ev} ${env_args}"
done
echo "${env_args}"
}
###################################################################################################
# container runner interface
#
# all container runners must edit the functions here
###################################################################################################
# 1. _denv_runner : print name and version of runner to stdout
_denv_runner() {
case "${denv_runner}" in
docker|podman|apptainer|singularity)
${denv_runner} --version
;;
norunner)
echo "norunner v0.0.0"
;;
*)
_denv_error "This should never happen."
exit 125
;;
esac
}
# 2. _denv_image_exists : check ${denv_image} exists locally
# - return 0 if it does exist, and 1 if it doesn't exist
_denv_image_exists() {
_denv_info "checking if ${denv_image} exists"
case "${denv_runner}" in
docker|podman)
images_matching_tag="$(${denv_runner} images -q "${denv_image}" 2> /dev/null)"
test -n "${images_matching_tag}"
return $?
;;
apptainer|singularity)
if [ -z "${denv_image_cache+x}" ]; then
test -e "${denv_image}"
return $?
else
[ -d "${denv_image_cache}" ] || return 1;
test -e "${denv_image_cache}/$(_denv_image_to_filename).sif"
return $?
fi
;;
norunner)
[ -d "${denv_image_cache}" ] || return 1;
test -f "${denv_image_cache}/$(_denv_image_to_filename)"
return $?
;;
*)
_denv_error "This should never happen"
return 125
;;
esac
}
# 3. _denv_pull : pull down ${denv_image} to a local copy
# - runners can use the directory ${denv_image_cache}
# to store image files if the runner requires the user to specify files
# - use _denv_image_to_filename function if you need a filename
# for a given image tag
_denv_pull() {
_denv_info "pulling ${denv_image}"
case "${denv_runner}" in
docker|podman)
${denv_runner} pull "${denv_image}"
;;
apptainer|singularity)
[ -d "${denv_image_cache}" ] || mkdir -p "${denv_image_cache}"
sif_file="${denv_image_cache}/$(_denv_image_to_filename).sif"
if [ -e "${denv_image}" ]; then
_denv_info "This path exists on the filesystem, assuming unpacked image."
# if the passed image is a file system path,
# we assume it is an image (unpacked or SIF)
# and so we just symlink to it in our cache
ln -sf "${denv_image}" "${sif_file}"
else
_denv_info "Image does not exist on filesystem, assuming registry tag."
# passed image is not a file so we assume it
# is the name of an image in some remote registry
# using the docker:// registry as default
image_full="${denv_image}"
echo "${denv_image}" | grep -v '://' && image_full="docker://${denv_image}"
if [ "${denv_runner}" = "apptainer" ]; then
container_env_var="APPTAINER_CONTAINER"
else
container_env_var="SINGULARITY_CONTAINER"
fi
# we pass the full image name including registry to be run by apptainer/singularity
# this then allows us to retrieve the full path to the cached SIF image file
# that is constructed from the OCI image
# then we link this cached image file to our local image cache
cached_image=$(${denv_runner} exec "${image_full}" printenv "${container_env_var}")
ln -sf "${cached_image}" "${sif_file}"
fi
;;
norunner)
mkdir -p "${denv_image_cache}"
touch "${denv_image_cache}/$(_denv_image_to_filename)"
;;
*)
_denv_error "This should never happen"
return 125
;;
esac
}
# 4. _denv_run : run the container
# - entrypoint is the denv-entrypoint program
# - use image ${denv_image}
# - set home directory in the container to ${denv_workspace}
# - set user and group in container to current user and group
# - attach all mounts in space-separated list "${denv_mounts}"
# - pass on all current environment variables
# as well as the DENV_* variables
# - set SHELL to be ${denv_shell} in container
_denv_run() {
_hostname="${denv_name}.$(uname -n || true)"
# we will be running now and not writing the config
# so we can update the denv_mounts list
[ -d /tmp/.X11-unix ] && denv_mounts="${denv_mounts} /tmp/.X11-unix"
if [ -n "${XAUTHORITY+x}" ] && [ -f "${XAUTHORITY}" ]; then
denv_mounts="${denv_mounts} ${XAUTHORITY}"
fi
denv_mounts="${denv_mounts} ${denv_entrypoint}"
case "${denv_runner}" in
docker|podman)
interactive=""
if tty -s; then
interactive="--interactive --tty"
fi
# and need to mount group definition file
# mounting the passwd and group files may not work if host or contianer OS do not look
# there for user/group definitions
# need to mount PWD just in case we are running outside of a mounted directory
denv_mounts="${denv_mounts} /etc/passwd /etc/group $(pwd -P)"
mounts="$(set +o xtrace; _denv_mounts_to_args "--volume")"
if [ "${denv_workspace}" != "$(pwd -P)" ]; then
# need to mount workspace manually for docker/podman
mounts="${mounts} --volume ${denv_workspace}:${denv_workspace}"
fi
environment="$(set +o xtrace; _denv_environment_to_args --env)"
userspec="--user $(id -u "${USER}"):$(id -g "${USER}")"
if [ "${denv_runner}" = "podman" ]; then
userspec="--userns=keep-id"
fi
network="host"
[ "${denv_network}" = "false" ] && network="none"
# shellcheck disable=SC2086,SC2248
${denv_runner} run \
--rm \
--network "${network}" \
${interactive} \
${mounts} \
--hostname "${_hostname}" \
${environment} \
--workdir "$(pwd -P)" \
--entrypoint "${denv_entrypoint}" \
${userspec} \
"${denv_image}" \
"$@"
;;
apptainer|singularity)
# apptainer/singularity do not pull automatically when attempting to run
_denv_config_checked_pull DO_NOT_PROMPT_USER
mounts="$(set +o xtrace; _denv_mounts_to_args "--bind")"
if [ -z "${denv_image_cache+x}" ]; then
# in a workspace-less shebang running mode, denv_image_cache is unset
# we assume the user has defined denv_image to be the full path to the image
# and rely on the runner to have a helpful error message about how the image
# is not found
sif_file="${denv_image}"
else
# in a workspace, we convert an image name to a filename that can
# reside in our local cache of image files
sif_file="${denv_image_cache}/$(_denv_image_to_filename).sif"
fi
environment="$(set +o xtrace; _denv_environment_to_args "--env")"
network=""
[ "${denv_network}" = "false" ] && network="--net --network none"
# shellcheck disable=SC2086
PWD=$(pwd -P) ${denv_runner} exec \
${network} \
--hostname "${_hostname}" \
--home "${denv_workspace}" \
--cleanenv \
${environment} \
${mounts} \
"${sif_file}" \
"${denv_entrypoint}" \
"$@"
;;
norunner)
network="enabled"
[ "${denv_network}" = "false" ] && network="disabled"
_denv_info "running ${denv_image} with " \
" HOME=\"${denv_workspace}\"" \
" SHELL=\"${denv_shell}\"" \
" PWD=\"$(pwd -P)\"" \
" network ${network}" \
"${denv_environment}" \
"${denv_mounts}"
;;
*)
_denv_error "This should never happen."
return 125
;;
esac
}
###################################################################################################
# denv internal helper functions
#
# these are the functions defining the CLI, loading the config, writing the config
# and calling the runner functions defined above in the correct sequence
###################################################################################################
# print the question passed to us and process user input
# continues in infinite loop if user is not explicitly answering yes or no
# Arguments
# 1 - question to ask the user (yes or no)
# Output
# returns 0 if user answers yes and 1 if user answers no
_denv_user_confirm() {
question="$*"
while true; do
printf "\033[1m%s\033[0m [Y/n] " "${question}"
read -r ans
case "${ans}" in
Y|y|yes)
return 0
;;
N|n|no)
return 1
;;
*)
_denv_error "${ans} is not one of Y, y, yes, N, n, or no."
;;
esac
done
}
_denv_deduce_workspace() {
# start from present working directory and go up directories until find .denv
# or reach root directory
denv_workspace="$(pwd -P || true)"
while [ "${denv_workspace}" != "/" ]; do
if [ -d "${denv_workspace}/.denv" ]; then
return 0
fi
denv_workspace="$(dirname "${denv_workspace}" || true)"
done
return 1
}
# deduce the runner we are going to run with
# Arguments
# DENV_RUNNER environment variable
_denv_deduce_runner() {
# make sure to update _denv_check and docs/src/manual/denv.md if the order of this elif tree is changed
if [ -n "${DENV_RUNNER+x}" ]; then
denv_runner="${DENV_RUNNER}"
if [ ! "${denv_runner}" = "norunner" ] && ! command -v "${denv_runner}" >/dev/null 2>&1; then
_denv_error "The container runner set via DENV_RUNNER=${DENV_RUNNER} is not a valid command." \
"You should only be setting DENV_RUNNER if you have more than one runner installed and wish to use denv with a specific one."
return 1
fi
elif command -v apptainer >/dev/null 2>&1; then
denv_runner="apptainer"
elif command -v singularity >/dev/null 2>&1; then
denv_runner="singularity"
elif command -v podman >/dev/null 2>&1; then
denv_runner="podman"
elif command -v docker >/dev/null 2>&1; then
denv_runner="docker"
else
_denv_error "No container runner available." \
"Setting 'norunner' which simply allows exploration of CLI."
denv_runner="norunner"
fi
_denv_info "using ${denv_runner} as runner"
}
# ^ means at beginning of the line being tested
# () groups all of those tests together
# | means OR so (one|two) means match one or two
# combining this means we are matching against env var prefixes listed
# in the parentheses separated by |
_denv_bad_env_var_name_regex="^(DENV|HOST|HOME|LANG|LC_CTYPE|.*PATH|SHELL|_|DISPLAY|X)"
# deduce the environment we should put into the denv
# Arguments
# parameters related to environment variables in a workspace config
# denv_env_var_copy_all denv_env_var_copy, denv_env_var_set
# denv_runner for HOME deduction
# Outputs
# denv_environment environment variable
_denv_deduce_environment() {
# deduce environment variables from configuration
# disable xtrace here to avoid polluting debug printout
set +o xtrace
# copy-able environment variables, original code copied from distrobox
# only get lines defining variables (with '=') and then remove lines
# with special characters (space, double-quote, back-tick, dollar-sign)
# or with special names (matching the regex above)
# The last grep checks for colons, if a line has a colon anywhere, an equals sign
# must be present before it (i.e. no colons in variable names).
copyable_environment="$(printenv | grep '=' | grep -Ev ' |"|`|\$' | grep -Ev "${_denv_bad_env_var_name_regex}=" | grep -E "^([^:]*|.*=.*:.*)=")"
if [ "${denv_env_var_copy_all}" = "true" ]; then
# deduce env variables, copied from distrobox
denv_environment="${copyable_environment}"
else
match_any_pattern="^($(echo "${denv_env_var_copy}" | sed 's/ /|/g'))="
denv_environment="$(echo "${copyable_environment}" | grep -E "${match_any_pattern}" || true)"
fi
denv_environment="${denv_environment} ${denv_env_var_set}"
# always add necessary environment variables
# - DENV_XXX variables for passing information to entrypoint
# - DISPLAY for connecting X11 GUIs launched from within container to host
# - This needs special consideration for MacOS hosts, we need
# to pass a special value to specify the intermediary VM's hostname
# - XAUTHORITY so the container X11 applications have the same authorization
# as the X11 applications launched outside a container on the host
# - HOME for mapping home directory in denv to workspace
_display="${DISPLAY:-:0}"
if [ "$(uname)" = "Darwin" ] && [ "${denv_runner}" = "docker" ]; then
# use the special value for docker to signal that we are attempting
# to connect to a host service from within the container
# https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
# MacOS users are required to install XQuartz and
# specify localhost as an allowed source
_display="host.docker.internal:0"
fi
[ -n "${XAUTHORITY+x}" ] && denv_environment="${denv_environment} XAUTHORITY=${XAUTHORITY}"
denv_environment="${denv_environment} DISPLAY=${_display}"
denv_environment="${denv_environment} DENV_NAME=${denv_name}"
denv_environment="${denv_environment} DENV_RUNNER=${denv_runner}"
denv_environment="${denv_environment} DENV_IMAGE=${denv_image}"
case "${denv_runner}" in
singularity|apptainer)
# should /not/ pass the HOME environment variable
# the program handles that when using the --home flag at run time
;;
*)
denv_environment="${denv_environment} HOME=${denv_workspace}"
;;
esac
if [ -n "${DENV_DEBUG+x}" ]; then
# add to the environment
denv_environment="${denv_environment} DENV_DEBUG=${DENV_DEBUG}"
fi
# env sifts out the unique variable names for us and uses the last value set
# this is the behavior we want since we add the `set` variables _after_
# the `copy` variables allowing the user to overwrite any host variables
# disabling double-quote warning so that we can re-split the space-separated list
# shellcheck disable=SC2086
denv_environment="$(env -i ${denv_environment})"
# re-instate xtrace if DENV_DEBUG was done
if [ -n "${DENV_DEBUG+x}" ]; then
set -o xtrace
fi
}
# load the denv config from the input workspace
# Arguments
# ${denv_workspace} env var
# Outputs
# loaded config into runtime shell environment
_denv_load_config() {
# deduce workspace if it isn't already defined
if [ -z "${denv_workspace+x}" ]; then
if ! _denv_deduce_workspace; then
_denv_error "Unable to deduce a denv workspace!" \
"Are you in a denv workspace? Do you still need to 'denv init'?"
return 1
fi
fi
# I am choosing to allow sourcing unknown code
# shellcheck disable=SC1091
. "${denv_workspace}/.denv/config"
denv_image_cache="${denv_workspace}/.denv/images"
_denv_deduce_runner
_denv_deduce_environment
}
# write the current config to the file
# Arguments
# ${denv_workspace} env var
# env - config variables from runtime env
_denv_write_config() {
config="${denv_workspace}/.denv/config"
{
echo "denv_name=\"${denv_name}\"";
echo "denv_image=\"${denv_image}\"";
echo "denv_shell=\"${denv_shell}\"";
echo "denv_mounts=\"${denv_mounts}\"";
echo "denv_env_var_copy_all=\"${denv_env_var_copy_all}\"";
echo "denv_env_var_copy=\"${denv_env_var_copy}\"";
echo "denv_env_var_set=\"${denv_env_var_set}\"";
echo "denv_network=\"${denv_network}\"";
} > "${config}"
}
# print the current denv config
# Arguments
# all denv_* env vars
# 1 - optional, if provided then print deduced env vars
# if we aren't copying the host env vars, then we will
# print them as well, expecting the list to be shorter
_denv_config_print() {
printf "%s=\"%s\"\n" denv_workspace "${denv_workspace}"
printf "%s=\"%s\"\n" denv_name "${denv_name}"
printf "%s=\"%s\"\n" denv_image "${denv_image}"
printf "%s=\"%s\"\n" denv_mounts "${denv_mounts}"
printf "%s=\"%s\"\n" denv_shell "${denv_shell}"
printf "%s=\"%s\"\n" denv_network "${denv_network}"
_denv_runner
if [ ! "${denv_env_var_copy_all}" = "true" ] || [ -n "${1:-}" ]; then
_denv_config_env print
fi
}
# pull down the ${denv_image} if it doesn't exist
# locally, if it does exist locally, check
# with the user if they wish for denv to pull
# Arguments
# 1 - disable user prompt if provided, just assume no pull
# denv_image (same as _denv_image_exists and _denv_pull)
_denv_config_checked_pull() {
if ! _denv_image_exists; then
_denv_pull
elif [ -z "${1+x}" ]; then
if _denv_user_confirm "${denv_image} already exists on this computer - re-pull it?"; then
_denv_pull
else
_denv_info "Not pulling ${denv_image}..."
fi
fi
}
# update the image used by the denv
# Arguments
# 1 - new image for the denv
_denv_config_image() {
case "$1" in
pull)
# if argument is keyword 'pull' then we simply
# pull down with the currently configured image tag
_denv_pull
;;
*)
# any other argument should be treated as the
# new image tag to use for the denv
denv_image="$1"
_denv_config_checked_pull "${DENV_NOPROMPT+DONT_PROMPT_USER}"
;;
esac
}
# set the shell to be used in the container
# Arguments
# 1+ - command to use as shell in the container
# Outputs
# sets denv_shell to the entire list of arguments
_denv_config_shell() {
denv_shell="$*"
}
# add another mount point for the container
# Arguments
# 1+ - path to directory to mount to container
# Outputs
# updates denv_mounts to include passed directories
_denv_config_mounts() {
while [ "$#" -gt "0" ]; do
case "$1" in
/*)
# this is a full path
if [ -d "$1" ]; then
_denv_info "Adding ${1} to mounts"
denv_mounts="$1 ${denv_mounts}"
else
_denv_error "'$1' does not exist."
return 1
fi
;;
*)
_denv_error "'$1' is not a full path"
return 1
;;
esac
shift
done
}
# enable/disable the network connection within the denv
_denv_config_network() {
case "$1" in
yes|true|on)
_denv_info "Enabling network connection"
denv_network="true"
;;
no|false|off)
_denv_info "Disabling network connection"
denv_network="false"
;;
*)
_denv_error "'$1' is not 'yes', 'on', or 'true' nor 'no', 'off', or 'false'"
;;
esac
}
# configure environment variables for within the denv
# Arguments
# 1 - {all,copy,set}
# Outputs
# updates the denv_env_var* config variables appropriately
_denv_config_env_help() {
cat <<\HELP
denv config env [help|-h|--help]
denv config env print
denv config env all {[yes|true|on]|[no|false|off]}
denv config env copy VAR0[=VAL0] [VAR1[=VAL1] ...]
denv config env reset
COMMANDS
help print this help and exit
print print the deduced environment variables that will be put into the container
all configure denv to copy "all" copy-able environment variables
into the denv (passing yes, true, or on), meaning any variables set to be copied manually
will be ignored, or disable this copy-all feature (by passing no, false, or off).
copy configure denv to copy specific environment variables (the VAR arguments)
into the denv. This disables the copy all feature as well.
Any argument with '=' is assumed to be a static KEY=VALUE pair to pass
into the denv as an environment variable while arguments without '=' have
the VALUE deduced from the host environment (or left unset if the host environment
is not set).
reset reset the environment variables to be copied into the denv to an empty state.
Clears the lists of environment variables to be copied and sets "all" to "no".
HELP
}
_denv_config_env() {
if [ -z "${1}" ]; then
_denv_config_env_help
return 1
fi
case "$1" in
help|-h|--help)
_denv_config_env_help
return 0
;;
print)
# disabling double-quote warning so that we can re-split the space-separated list
# shellcheck disable=SC2086
printf "%s\n" ${denv_environment}
;;
all)
shift
if [ "$#" -eq "0" ]; then
# default with no arguments is yes
_denv_info "Copying all copyable host environment variables."
denv_env_var_copy_all="true"
return 0;
fi
case "$1" in
no|false|off)
_denv_info "Only copying envrionment variables specified by user."
denv_env_var_copy_all="false"
;;
yes|true|on)
_denv_info "Copying all copyable host environment variables."
denv_env_var_copy_all="true"
;;
*)
_denv_error "'$1' is not 'yes', 'on', or 'true' nor 'no', 'off', or 'false'"
return 1;
;;
esac
;;
copy)
shift
while [ "$#" -gt "0" ]; do
# we are only handling one variable and not lines of variables here
# so we can take more care, we first check that the name doesn't match the bad name regex
# or has any colons in it
# the cut separates out the name by the first equals sign and so we know to fail anything
# that has any colon in it
if echo "${1}" | cut -f 1 -d = | grep -E "${_denv_bad_env_var_name_regex}|(.*:.*)"; then
_denv_error "The passed variables named above are special and should not be used in the denv."
return 1
fi
if echo "${1}" | grep -E ' |"|`|\$'; then
_denv_error "Values cannot use special characters (space ' ', quote '\"', tick '\`', dollar-sign '$')"
return 1
fi
if echo "${1}" | grep -q "="; then
_denv_info "Setting environment variable in denv '${1}'."
if [ -z "${denv_env_var_set}" ]; then
denv_env_var_set="${1}"
else
denv_env_var_set="${denv_env_var_set} ${1}"
fi
else
if [ "${denv_env_var_copy_all}" = "true" ]; then
_denv_error "denv is configured to copy all environment variables from the host already." \
"If you wish to only copy specific variables, disable copy-all first with 'denv config env all off'."
return 1
fi
_denv_info "Copying host environment variable in denv '${1}'."
if [ -z "${denv_env_var_copy}" ]; then
denv_env_var_copy="${1}"
else
denv_env_var_copy="${denv_env_var_copy} ${1}"
fi
fi
shift
done
;;
reset)
_denv_info "Copying all copyable host environment variables and clearing previous variable lists."
denv_env_var_copy_all="true"
denv_env_var_copy=""
denv_env_var_set=""
;;
*)
_denv_error "'$1' is not a recognized keyword 'help', 'print', 'all', 'copy', or 'reset'"
return 1
;;
esac
}
# config some config variable
# Arguments
# 1 - varible to config
# 2 - new value for that variable
_denv_config_help() {
cat <<\HELP
denv config [help|-h|--help]
denv config print [include-env]
denv config image [pull|IMAGE]
denv config mounts DIR0 [DIR1 DIR2 ...]
denv config shell SHELL
denv config network {[yes|true|on]|[no|false|off]}
denv config env [args...]
COMMANDS
help print this help and exit
print print the deduced config for viewing
if an argument is provided, then show the environment variables as well
image set the image you wish to use
the special keyword 'pull' can be used to
not change the image being used but to re-download
the same tag from the remote repository
mounts add one or more directories to be mounted to the denv
shell change which program is executed when opening
the denv without any arguments
network enable connecting the network to the denv (passing yes, true, or on),
or disable connecting the network (passing no, false, or off).
env configure which environment variables are put
into the container environment
`denv config env help` for more detail
HELP
}
_denv_config() {
if [ $# -eq 0 ]; then
_denv_config_help
return 0
fi
case "$1" in
print)
_denv_load_config
cmd="_denv_config_${1}"
${cmd} "${2:-}"
;;
image|shell|mounts|network|env)
if [ "$1" = "env" ]; then
# need to check for help printout before loading config
# so users can access help information without a denv
if [ $# -eq 1 ]; then
_denv_config_env_help
exit 0
fi
case "$2" in
help|-h|--help)
_denv_config_env_help
exit 0
;;
*)
# fall through to command handling below
;;
esac
fi
_denv_load_config
cmd="_denv_config_${1}"
if [ "$#" -eq "1" ]; then
_denv_config_help
_denv_error "$1 requires an argument."
return 1
fi
shift
# I think I know what I'm doing
# shellcheck disable=SC2068
${cmd} $@
_denv_write_config
;;
help|-h|--help)
_denv_config_help
;;
*)
_denv_error "Unrecognized config argument '$1'"
return 1
;;
esac
}
# create a new denv workspace
_denv_init_help() {
cat <<\HELP
denv init [OPTIONS] IMAGE [WORKSPACE]
ARGUMENT
help : print this help and exit
IMAGE : the container image to use for running the denv
WORKSPACE : the directory to create the denv for
optional, defaults to present working directory
OPTIONS
-h, --help : print this help and exit
--no-gitignore : don't generate a gitignore for the .denv directory
--[no-]over : Don't ask about overwrite/override.
Instead just do it (--over) or don't do it (--no-over).
--[no-]mkdir : Don't ask about creating WORKSPACE if it doesn't exist.
Instead just do it (--mkdir) or don't (--no-mkdir).
--force : alias for --over --mkdir
--name : set a name for this denv
--clean-env : don't enable copying of host environment variables
--no-copy-all is another alias which more closely
mimics the underlying implementation and wording of
'denv config env'
HELP
}
_denv_init() {
# check for help request
if [ $# -eq 0 ]; then
_denv_init_help
_denv_error "Provide at least an image to use for running"
return 1
fi
copyall=true
gitignore=true
positionals=""
while [ "$#" -gt "0" ]; do
case "$1" in
help|-h|--help)
_denv_init_help
return 0
;;
--no-gitignore)
gitignore=false
;;
--clean-env|--no-copy-all)
copyall=false
;;
--force)
over=true
mkwork=true
;;
--no-over)
over=false
;;
--over)
over=true
;;
--no-mkdir)
mkwork=false
;;
--mkdir)
mkwork=true
;;
--name)
if [ -z "${2+x}" ]; then
_denv_error "The '$1' option requires an argument after it."
return 1
fi
denv_name="${2}"
shift
;;
*)
positionals="${positionals} ${1}"
;;
esac
shift
done
# I want to re-split the positional arguments because neither
# of them should have a space inside
# shellcheck disable=SC2086
set -- ${positionals}
# image - first positional and only required argument
image="$1"
shift
# workspace - second positional argument if given, otherwise PWD
if [ ! "$#" -eq 0 ]; then
denv_workspace="$1"
# check if this directory exists, if not create it
if [ ! -d "${denv_workspace}" ]; then
if [ -n "${mkwork+x}" ]; then
if ${mkwork}; then
_denv_info "user allows denv to create ${denv_workspace}"
else
_denv_info "refusing to create a directory per user instruction"
return 0
fi
elif [ -n "${DENV_NOPROMPT+x}" ]; then
_denv_error "denv prompt disabled but unwilling to create a directory without user input."
return 1
elif ! _denv_user_confirm "This directory does not exist. Would you like to create it for the denv?"; then
_denv_info "exiting without creating '${denv_workspace}'..."
return 0
fi
_denv_info "creating '${denv_workspace}' for the denv..."
mkdir "${denv_workspace}"
fi
_denv_info "entering '${denv_workspace}'..."
cd "${denv_workspace}"
fi
# check if there is a workspace here or "above" (in a parent directory)
if _denv_deduce_workspace; then
verb="overrid"
if [ "${denv_workspace}" = "${PWD}" ]; then
verb="overwrit"
fi
if [ -n "${over+x}" ]; then
if ${over}; then
_denv_info "${verb}ing previous denv workspace at ${denv_workspace}"
else
_denv_info "refusing to ${verb}e previous denv workspace per user instruction"
return 0
fi
elif [ -n "${DENV_NOPROMPT+x}" ]; then
_denv_error "denv prompt disabled but unwilling to ${verb}e a denv without user input."
return 1
elif ! _denv_user_confirm "This workspace already has a denv (in ${denv_workspace}). Would you like to ${verb}e it?"; then
_denv_info "exiting without ${verb}ing denv within '${denv_workspace}'..."
return 0
fi
fi
# if the workspace was given, we have created and entered it,
# if it wasn't, we are already in it, so the workspace is PWD
denv_workspace="${PWD}"
# create directory if it doesn't exist,
# we need to check in case we are re-initing on top of a denv
[ -d "${denv_workspace}/.denv" ] || mkdir "${denv_workspace}/.denv"
# set the default denv name to the workspace directory name
if [ -z "${denv_name+x}" ]; then
denv_name="$(basename "${denv_workspace}")"
fi
# we have a clean workspace directory, lets make a new denv
denv_image="${image}"
denv_shell="/bin/bash -i"
denv_mounts=""
denv_network="true"
denv_env_var_copy_all="${copyall}"
denv_env_var_copy=""
denv_env_var_set=""
_denv_write_config
if ${gitignore}; then
_denv_info "writing a gitignore for the .denv directory."
cat > "${denv_workspace}/.denv/.gitignore" <<\GITIGNORE
# ignore everything in this directory
*
# except the config which folks might want to share
# across many computers and this ignore file itself
!config
!.gitignore
GITIGNORE
fi