-
Notifications
You must be signed in to change notification settings - Fork 0
/
.manage
520 lines (440 loc) · 12.3 KB
/
.manage
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
#!/usr/bin/env bash
BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_HOSTS="$BASE/meta/hosts"
BASE_CONFIGS="$BASE/meta/configs"
BASE_PARTS="$BASE/meta/parts"
###
# Color management
###
_RST=$'\x1b'"[0m"; _BLD=$'\x1b'"[1m"; _BCK=$'\x1b'"[30m"
_RED=$'\x1b'"[31m"; _GRN=$'\x1b'"[32m"; _BRW=$'\x1b'"[33m"
_BLU=$'\x1b'"[34m"; _MTA=$'\x1b'"[35m"; _CYN=$'\x1b'"[36m"
if [ -t 1 ]; then
# stdout is a terminal
_OUT_RST="$_RST"; _OUT_BLD="$_BLD"; _OUT_BCK="$_BCK"
_OUT_RED="$_RED"; _OUT_GRN="$_GRN"; _OUT_BRW="$_BRW"
_OUT_BLU="$_BLU"; _OUT_MTA="$_MTA"; _OUT_CYN="$_CYN"
else
# stdout is not a terminal
_OUT_RST=""; _OUT_BLD=""; _OUT_BCK=""
_OUT_RED=""; _OUT_GRN=""; _OUT_BRW=""
_OUT_BLU=""; _OUT_MTA=""; _OUT_CYN=""
fi
if [ -t 2 ]; then
# stderr is a terminal
_ERR_RST="$_RST"; _ERR_BLD="$_BLD"; _ERR_BCK="$_BCK"
_ERR_RED="$_RED"; _ERR_GRN="$_GRN"; _ERR_BRW="$_BRW"
_ERR_BLU="$_BLU"; _ERR_MTA="$_MTA"; _ERR_CYN="$_CYN"
else
# stderr is not a terminal
_ERR_RST=""; _ERR_BLD=""; _ERR_BCK=""
_ERR_RED=""; _ERR_GRN=""; _ERR_BRW=""
_ERR_BLU=""; _ERR_MTA=""; _ERR_CYN=""
fi
# check minimal Bash version
if ! (( BASH_VERSINFO[0] > 4 || \
(BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 3) )); then
echo "[${_ERR_RED}!${_ERR_RST}] Requires at least Bash 4.3" >&2
exit 2
fi
###
# Base functions
###
function initialize_workspace() {
# the current host file is used to keep track of initialized workspaces
local file="$(get_current_host_file)"
if ! [[ -f "$file" ]]; then
# load submodules
update_submodules
# create meta directories
mkdir -p "$BASE_HOSTS" "$BASE_CONFIGS" "$BASE_PARTS"
# create an empty current host file
save_current_host
fi
}
function update_submodules() {
echo "[${_OUT_BLU}*${_OUT_RST}] Updating submodules"
(
cd "$BASE"
# Update submodules. Only match remote for 1st level
git submodule update --init --remote
git add . &> /dev/null
git submodule update --init --recursive
git reset &> /dev/null
)
# Keep some submodules at specific versions
(
cd "$BASE/vendors/dotbot"
git checkout v1.19.0
)
}
###
# HOST MANAGEMENT
###
function get_host_file() {
echo "$BASE_HOSTS/$1"
}
function list_host_files() {
(
cd "$BASE_HOSTS"
find . -type f | cut -d / -f 2- | sort
)
}
function host_exists() {
local host="$1"
local file="$(get_host_file "$host")"
if ! [[ -f "$file" && -r "$file" ]]; then
return 1
fi
}
function load_host() {
local host="$1"
local file="$(get_host_file "$host")"
if ! host_exists "$host"; then
echo "[${_ERR_RED}!${_ERR_RST}] Unknown host '$host'" >&2
return 1
fi
load_host_file "$file"
__current_host_init=1
}
function save_host() {
local host="$1"
local file="$(get_host_file "$host")"
if host_exists "$hosts"; then
echo "[${_OUT_BRW}i${_OUT_RST}] File of host '$host' will be overridden"
fi
# update file with current file
get_current_host > "$file"
# commit
( cd "$BASE"
git add "$file"
git commit \
--message "Automated commit: Update of host '$host' on $(date "+%Y-%m-%d %H:%M:%S")" \
-- "$file"
)
echo "[${_OUT_BLU}*${_OUT_RST}] Host '$host' saved and committed"
}
###
# CURRENT HOST MANAGEMENT
###
function get_current_host_file() {
echo "$BASE/.current"
}
declare __current_host=""
declare __current_host_init=0
function load_host_file() {
local file="$1"
if [[ -f "$file" && -r "$file" ]]; then
while IFS= read -r line; do
# skip comments
if [[ "$line" == "#"* ]]; then continue; fi
# resolve config
if config_exists "$line"; then
if ! current_host_has "$line"; then
__current_host+="$line"$'\n'
fi
else
echo "[${_ERR_RED}!${_ERR_RST}] Invalid config in host: $line" >&2
return 1
fi
done <"$file"
elif [[ -e "$file" ]]; then
echo "[${_ERR_RED}!${_ERR_RST}] $file exists but is not readable" >&2
return 1
fi
}
function current_host_has() {
local config="$1"
if ! grep -x -q "$config" <<<"$__current_host"; then
return 1
fi
}
function load_current_host() {
if [[ $__current_host_init == 0 ]]; then
local file="$(get_current_host_file)"
load_host_file "$file"
__current_host_init=1
fi
}
function get_current_host() {
load_current_host
echo -n "$__current_host"
}
function clear_current_host() {
__current_host=""
__current_host_init=1
}
function add_current_host() {
load_current_host
local config="$1"
# only add if not already in it
if ! current_host_has "$config"; then
__current_host+="$config"$'\n'
fi
}
function remove_current_host() {
load_current_host
local config="$1"
# we need to manually append with newline because shell substitution will
# strip it
__current_host="$(grep -x -v "$config" <<<"$__current_host")"$'\n'
}
function save_current_host() {
local file="$(get_current_host_file)"
if [[ -e "$file" && (! -f "$file" || ! -w "$file") ]]; then
echo "[${_ERR_RED}!${_ERR_RST}] Cannot save current configuration to $file" >&2
return 1
fi
echo "# This is a managed file. It should not be updated manually" >"$file"
echo -n "$__current_host" >>"$file"
}
###
# CONFIG MANAGEMENT
###
function get_config_file() {
echo "$BASE_CONFIGS/$1"
}
function list_config_files() {
(
cd "$BASE_CONFIGS"
find . -type f | cut -d / -f 2- | sort
)
}
function config_exists() {
local config="$1"
local file="$(get_config_file "$config")"
if ! [[ -f "$file" && -r "$file" ]]; then
return 1
fi
}
function simple_templater() {
local file="$1"
# use eval to provide bash syntax
eval "cat <<_0f547162_EOF
$(cat "$file")
_0f547162_EOF"
return $?
}
function resolve_config() {
local config="$1"
local -a parts
# check config file existance
local file
file="$(get_config_file "$config")"
if [[ $? != 0 ]]; then
echo "[${_ERR_RED}!${_ERR_RST}] Config file for '$config' does not exist" >&2
return 1
fi
# template the config
local templated_config
templated_config="$(simple_templater "$file")"
if [[ $? != 0 ]]; then
echo "[${_ERR_RED}!${_ERR_RST}] Invalid config '$config': fail to template it" >&2
return 1
fi
while IFS= read -r line; do
# skip comments and empty lines
if [[ "$line" == "#"* ]] || [[ "$line" == "" ]]; then continue; fi
# resolve config
if [[ "$line" == "dependency="* ]]; then
# check dependancies
if config_exists "${line#dependency=}"; then
parts+=("$line")
else
echo "[${_ERR_RED}!${_ERR_RST}] Invalid dependency in config '$config': ${line#dependency=}" >&2
return 1
fi
else
# check parts
if part_exists "$line"; then
parts+=("$line")
else
echo "[${_ERR_RED}!${_ERR_RST}] Invalid part in config '$config': $line" >&2
return 1
fi
fi
done <<< "$templated_config"
(IFS=$'\n'; echo "${parts[*]}")
}
###
# PART MANAGEMENT
###
function get_part_file() {
echo "$BASE_PARTS/$1.yaml"
}
function list_part_files() {
(
cd "$BASE_PARTS"
find . -type f | cut -d / -f 2- | sed "s/\.yaml$//" | sort
)
}
function part_exists() {
local part="$1"
local file="$(get_part_file "$part")"
if ! [[ -f "$file" && -r "$file" ]]; then
return 1
fi
}
###
# INTERFACE WITH DOTBOT
###
function install_all_configs() {
# install a list of config
# CRC32(to_load@install_all_configs) = bcb044c5
local -n _bcb044c5_to_load="$1"
local verbose="${2:-}"
# loaded is used to load config only once, while keeping the order
local -A loaded=()
local config
# ensure current host config is loaded to be able to template the configs
load_current_host
for config in "${_bcb044c5_to_load[@]}"; do
install_all_configs_recurse "$config"
# stop here if install failed
if [[ $? != 0 ]]; then return 1; fi
done
}
function install_all_configs_recurse() {
# use to install config recursively, for dependencies
local config="$1"
local dependency_of="${2:-}"
# ensure only not loaded config are
if [[ ${loaded[$config]} == 1 ]]; then return; fi
if [[ "$dependency_of" == "" ]]; then
echo "[${_OUT_BLU}*${_OUT_RST}] Loading config '$config'"
else
echo "[${_OUT_BLU}*${_OUT_RST}] Loading '$config' dependency of '$dependency_of'"
fi
# resolve the config file
local config_content
config_content="$(resolve_config "$config")"
# stop here if resolution failed
if [[ $? != 0 ]]; then return 1; fi
# install dependencies recursively
if grep -q '^dependency=' <<<"$config_content"; then
local dep
while read -r dep; do
install_all_configs_recurse "${dep#dependency=}" "$config"
done <<<"$(grep '^dependency=' <<<"$config_content")"
echo "[${_OUT_BLU}*${_OUT_RST}] All dependencies installed, installing '$config'"
fi
# install the config
if ! install_config $verbose <<<"$(grep -v '^dependency=' <<<"$config_content")"; then
echo "[${_ERR_RED}!${_ERR_RST}] Error while loading config $config" >&2
echo "[${_ERR_RED}!${_ERR_RST}] Please fix the config file and run '$SCRIPT update'" >&2
return 1
fi
# mark config as loaded
loaded[$config]=1
}
function install_config() {
local base_part="$BASE/meta/base.yaml"
local end_part="$BASE/meta/end.yaml"
# read the config from input
readarray -t parts
# remove previously generated configs
for f in /tmp/dotfiles.cfg.??????.yaml; do
if [[ -f "$f" ]]; then
rm -f "$f"
fi
done
# ensure secrets are decrypted before doing anything
decrypt_secrets "noforce" "dummy"
# create the temporary config file
local config_file="$(mktemp -p "/tmp" "dotfiles.cfg.XXXXXX.yaml")"
# base.yaml
if [[ -f "$base_part" && -r "$base_part" ]]; then
echo "$(<"$base_part")" >> "$config_file"
fi
# concat each part
for f in "${parts[@]}"; do
# skip empty lines (may append when no parts are configured)
if [[ "$f" == "" ]]; then continue; fi
cat "$(get_part_file "$f")" >> "$config_file"
done
# end.yaml
if [[ -f "$end_part" && -r "$end_part" ]]; then
echo "$(<"$end_part")" >> "$config_file"
fi
python3 "$BASE/vendors/dotbot/bin/dotbot" \
--base-directory "$BASE/dotfiles" \
--config-file "$config_file" \
--plugin-dir "$BASE/plugins" \
${DOTFILES_DEBUG:+-v} \
"$@"
local exitcode=$?
# remove file if all went well
if [[ $exitcode == 0 ]]; then
rm -f "$config_file"
fi
return $exitcode
}
###
# SECRET MANAGEMENT
###
function add_to_secrets() {
local file="$1"
local abs_path="$PWD/$file"
# ensure a file was given
if ! [ -f "$file" ]; then
echo "[${_ERR_RED}!${_ERR_RST}] '$file' is not a valid file" >&2
return 1
fi
# ensure the file is added to gitignore
sed -ri "/## Start secrets section/a\\${abs_path#$BASE}" "$BASE/.gitignore"
# Add the file to git-secret
"$BASE/vendors/git-secret" add "$file"
# ensure we start creating an encrypted version of the file
"$BASE/vendors/git-secret" hide
}
function remove_from_secrets() {
local file="$1"
local abs_path="$PWD/$file"
# ensure a file was given
if ! [ -f "$file" ]; then
echo "[${_ERR_RED}!${_ERR_RST}] '$file' is not a valid file" >&2
return 1
fi
"$BASE/vendors/git-secret" remove "$file"
# ensure the file is removed from gitignore
sed -ri "/## Start secrets section/,/## End secrets section/{ \\|${abs_path#$BASE}|d }" "$BASE/.gitignore"
}
function encrypt_secrets() {
"$BASE/vendors/git-secret" hide -d
}
function decrypt_secrets() {
local force="$1" dummy="$2"
# Only decrypt if some files cannot be found
local secret_files="$(sed -n \
'/## Start secrets section/,/## End secrets section/{ \|^\s*[^#\t ]|p }' \
"$BASE/.gitignore"
)"
local need_refresh="n"
while read -r file; do
if ! [ -f "$BASE$file" ]; then
need_refresh="y"
break
fi
done <<<"$secret_files"
if [[ "$need_refresh" == "y" || "$force" == "force" ]]; then
if [[ "$need_refresh" == "y" ]]; then
echo "Need to decrypt secret files"
else
echo "Force to decrypt secret files"
fi
if "$BASE/vendors/git-secret" reveal; then
return 0
elif [[ "$dummy" == "dummy" ]]; then
# create dummy secret files if asked for and secret reveal failed
echo "[${_OUT_BRW}i${_OUT_RST}] Some secret could not be decryted: create dummy files"
while read -r file; do
if ! [ -f "$BASE$file" ]; then
touch "$BASE$file"
fi
done <<<"$secret_files"
return 0
else
return 1
fi
fi
}