inital commit
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
#compdef dcsctl
|
||||
|
||||
# --- helpers ---------------------------------------------------------------
|
||||
|
||||
_dcsctl__docker_contexts() {
|
||||
local -a ctxs
|
||||
ctxs=(${(f)"$(command docker context ls --format '{{.Name}}' 2>/dev/null)"})
|
||||
_describe -t docker-contexts 'docker contexts' ctxs
|
||||
}
|
||||
|
||||
_dcsctl__current_context() {
|
||||
command docker context show 2>/dev/null
|
||||
}
|
||||
|
||||
_dcsctl__control_plane_base() {
|
||||
# Match dcsctl logic: base is $DCS_ROOT if set, else ~/.dcs
|
||||
print -r -- "${DCS_ROOT:-$HOME/.dcs}"
|
||||
}
|
||||
|
||||
_dcsctl__projects() {
|
||||
local ctx base
|
||||
ctx="$(_dcsctl__current_context)"
|
||||
[[ -z "$ctx" ]] && return 0
|
||||
|
||||
base="$(_dcsctl__control_plane_base)/$ctx/compose"
|
||||
[[ -d "$base" ]] || return 0
|
||||
|
||||
local -a ignored projects
|
||||
ignored=(shared '@Recycle' '@Recently-Snapshot' '.DS_Store')
|
||||
|
||||
projects=()
|
||||
local dir name
|
||||
for dir in "$base"/*; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
name="${dir:t}"
|
||||
(( ${ignored[(I)$name]} )) && continue
|
||||
projects+=("$name")
|
||||
done
|
||||
|
||||
_describe -t dcs-projects 'projects' projects
|
||||
}
|
||||
|
||||
_dcsctl__secret_names_from_dir() {
|
||||
local ctx proj base projdir secretdir
|
||||
ctx="$(_dcsctl__current_context)"
|
||||
[[ -z "$ctx" ]] && return 1
|
||||
|
||||
proj="${words[4]}" # dcsctl secret add <project> <name>
|
||||
[[ -z "$proj" ]] && return 1
|
||||
|
||||
base="$(_dcsctl__control_plane_base)/$ctx/compose"
|
||||
projdir="$base/$proj"
|
||||
secretdir="$projdir/secrets"
|
||||
[[ -d "$secretdir" ]] || return 1
|
||||
|
||||
local -a names
|
||||
names=()
|
||||
local f n
|
||||
for f in "$secretdir"/*.txt; do
|
||||
[[ -f "$f" ]] || continue
|
||||
n="${f:t}"
|
||||
n="${n%.txt}"
|
||||
names+=("$n")
|
||||
done
|
||||
|
||||
(( ${#names} == 0 )) && return 1
|
||||
_describe -t dcs-secrets 'secret names' names
|
||||
}
|
||||
|
||||
# List service subdirectories in a swarm-mode project.
|
||||
# Takes one argument: the project name.
|
||||
_dcsctl__swarm_services() {
|
||||
local ctx proj base projdir svcdir
|
||||
ctx="$(_dcsctl__current_context)"
|
||||
[[ -z "$ctx" ]] && return 1
|
||||
|
||||
proj="$1"
|
||||
[[ -z "$proj" ]] && return 1
|
||||
|
||||
base="$(_dcsctl__control_plane_base)/$ctx/compose"
|
||||
projdir="$base/$proj"
|
||||
svcdir="$projdir/services"
|
||||
[[ -d "$svcdir" ]] || return 1
|
||||
|
||||
local -a svcs
|
||||
svcs=()
|
||||
local dir name
|
||||
for dir in "$svcdir"/*/; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
name="${dir:t}"
|
||||
svcs+=("$name")
|
||||
done
|
||||
|
||||
(( ${#svcs} == 0 )) && return 1
|
||||
_describe -t dcs-swarm-services 'services' svcs
|
||||
}
|
||||
|
||||
# Detect whether a project is in swarm mode (has services/ dir).
|
||||
_dcsctl__is_swarm() {
|
||||
local ctx proj base projdir
|
||||
ctx="$(_dcsctl__current_context)"
|
||||
[[ -z "$ctx" ]] && return 1
|
||||
|
||||
proj="$1"
|
||||
[[ -z "$proj" ]] && return 1
|
||||
|
||||
base="$(_dcsctl__control_plane_base)/$ctx/compose"
|
||||
projdir="$base/$proj"
|
||||
[[ -d "$projdir/services" ]]
|
||||
}
|
||||
|
||||
# Fetch services for a given project by asking docker compose for the resolved config.
|
||||
_dcsctl__services() {
|
||||
local ctx proj base projdir
|
||||
ctx="$(_dcsctl__current_context)"
|
||||
[[ -z "$ctx" ]] && return 1
|
||||
|
||||
proj="${words[3]}" # dcsctl run <project> ...
|
||||
[[ -z "$proj" ]] && return 1
|
||||
|
||||
base="$(_dcsctl__control_plane_base)/$ctx/compose"
|
||||
projdir="$base/$proj"
|
||||
[[ -d "$projdir" ]] || return 1
|
||||
|
||||
local env1 env2 env3
|
||||
env1="$projdir/.env"
|
||||
env2="$projdir/service.env"
|
||||
env3="$projdir/service.secrets.env"
|
||||
|
||||
[[ -f "$env1" && -f "$env2" && -f "$env3" && -f "$projdir/docker-compose.yml" ]] || return 1
|
||||
|
||||
local -a svcs
|
||||
svcs=(${(f)"$(
|
||||
command docker --context "$ctx" compose \
|
||||
--project-directory "$projdir" \
|
||||
--project-name "$proj" \
|
||||
--env-file "$env1" \
|
||||
--env-file "$env2" \
|
||||
--env-file "$env3" \
|
||||
config --services 2>/dev/null
|
||||
)"})
|
||||
|
||||
(( ${#svcs} == 0 )) && return 1
|
||||
_describe -t dcs-services 'services' svcs
|
||||
}
|
||||
|
||||
# Heuristic: decide if we are in a position where a service name is expected.
|
||||
_dcsctl__maybe_complete_service() {
|
||||
local subcmd cur
|
||||
subcmd="${words[4]}" # dcsctl run <project> <subcmd> ...
|
||||
cur="${words[CURRENT]}"
|
||||
|
||||
[[ "$cur" == -* ]] && return 1
|
||||
|
||||
case "$subcmd" in
|
||||
logs|ps|stop|start|restart|up|pull|build|rm|kill|pause|unpause|top|exec)
|
||||
_dcsctl__services
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Delegate completion to docker after we reconstruct words as docker compose ...
|
||||
_dcsctl__docker_compose_delegate() {
|
||||
local ctx proj base projdir
|
||||
ctx="$(_dcsctl__current_context)"
|
||||
[[ -z "$ctx" ]] && return 1
|
||||
|
||||
proj="${words[3]}"
|
||||
[[ -z "$proj" ]] && return 1
|
||||
|
||||
base="$(_dcsctl__control_plane_base)/$ctx/compose"
|
||||
projdir="$base/$proj"
|
||||
|
||||
local -a dc_words
|
||||
dc_words=(docker --context "$ctx" compose --project-directory "$projdir" --project-name "$proj")
|
||||
|
||||
if (( ${#words} >= 4 )); then
|
||||
dc_words+=("${words[@]:3}") # from 4th element
|
||||
fi
|
||||
|
||||
local -a save_words
|
||||
local save_cword
|
||||
save_words=("${words[@]}")
|
||||
save_cword=$CURRENT
|
||||
|
||||
words=("${dc_words[@]}")
|
||||
CURRENT=${#words}
|
||||
|
||||
autoload -Uz _docker
|
||||
_docker
|
||||
|
||||
words=("${save_words[@]}")
|
||||
CURRENT=$save_cword
|
||||
}
|
||||
|
||||
# Helper for edit --service completion: extract the project arg from the command
|
||||
# line and complete swarm service names.
|
||||
_dcsctl__swarm_services_for_edit() {
|
||||
local proj=""
|
||||
local i
|
||||
for (( i=3; i <= ${#words}; i++ )); do
|
||||
local w="${words[$i]}"
|
||||
case "$w" in
|
||||
-e|--env|-r|--runtime|-s|--secret) ;;
|
||||
--service) (( i++ )) ;; # skip the next arg (the service value)
|
||||
-*) ;;
|
||||
*)
|
||||
if [[ -z "$proj" ]]; then
|
||||
proj="$w"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$proj" ]] && return 1
|
||||
_dcsctl__swarm_services "$proj"
|
||||
}
|
||||
|
||||
# --- main completion -------------------------------------------------------
|
||||
|
||||
_dcsctl() {
|
||||
local -a subcmds
|
||||
subcmds=(
|
||||
'new:Create a new project scaffold'
|
||||
'verify:Verify and enforce host permissions for appdata bind mounts'
|
||||
'run:Run docker compose for a project (compose mode)'
|
||||
'up:Alias for run <project> up -d (compose mode)'
|
||||
'down:Bring down a project (compose or swarm)'
|
||||
'stop:Scale all services to 0 replicas (swarm projects only)'
|
||||
'deploy:Merge service fragments and deploy as Docker Swarm stack'
|
||||
'ls:Alias for docker compose ls'
|
||||
'dir:Print the project directory, or a service directory in a swarm project'
|
||||
'edit:Edit project files in $EDITOR'
|
||||
'secret:Manage project secrets'
|
||||
'service:Manage services within a swarm project'
|
||||
'import:Import an existing docker-compose.yml into a DCS project'
|
||||
'context:Manage DCS control-plane contexts'
|
||||
'reload:Down, rebuild and up all containers within a DCS project'
|
||||
'rename:Rename and update a DCS project'
|
||||
'help:Show help for a command'
|
||||
)
|
||||
|
||||
if (( CURRENT == 2 )); then
|
||||
_describe -t commands 'dcsctl commands' subcmds
|
||||
return
|
||||
fi
|
||||
|
||||
local cmd
|
||||
cmd="${words[2]}"
|
||||
|
||||
case "$cmd" in
|
||||
new)
|
||||
_arguments -C \
|
||||
'(-v --verify)'{-v,--verify}'[Run verification after creating the project]' \
|
||||
'--verbose[Verbose output for verification (used with --verify)]' \
|
||||
'--compose[Create flat single-compose layout (legacy)]' \
|
||||
'--service[Initial service name for swarm layout]:service name:' \
|
||||
'1:project name:' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
verify)
|
||||
_arguments -C \
|
||||
'--verbose[Show current vs expected ownership/mode while verifying (QNAP only)]' \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
run)
|
||||
if (( CURRENT == 3 )); then
|
||||
_dcsctl__projects
|
||||
return
|
||||
fi
|
||||
|
||||
if (( CURRENT >= 5 )); then
|
||||
_dcsctl__maybe_complete_service && return
|
||||
fi
|
||||
|
||||
_dcsctl__docker_compose_delegate
|
||||
return
|
||||
;;
|
||||
|
||||
up)
|
||||
_arguments -C \
|
||||
'(-v --verify)'{-v,--verify}'[Run `dcsctl verify` before running docker compose]' \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
down)
|
||||
_arguments -C \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
stop)
|
||||
_arguments -C \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
deploy)
|
||||
_arguments -C \
|
||||
'(-v --verify)'{-v,--verify}'[Run verification before deploying]' \
|
||||
'--verbose[Verbose verification output]' \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
ls)
|
||||
return
|
||||
;;
|
||||
|
||||
dir)
|
||||
if (( CURRENT == 3 )); then
|
||||
_dcsctl__projects
|
||||
return
|
||||
fi
|
||||
if (( CURRENT == 4 )); then
|
||||
local proj="${words[3]}"
|
||||
_dcsctl__swarm_services "$proj" && return
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
|
||||
edit)
|
||||
_arguments -C \
|
||||
'(-e --env -r --runtime -s --secret)'{-e,--env}'[Edit .env]' \
|
||||
'(-e --env -r --runtime -s --secret)'{-r,--runtime}'[Edit service.env]' \
|
||||
'(-e --env -r --runtime -s --secret)'{-s,--secret}'[Edit service.secrets.env]' \
|
||||
'--service[Target a specific service (swarm mode)]:service name:_dcsctl__swarm_services_for_edit' \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
secret)
|
||||
if (( CURRENT == 3 )); then
|
||||
_describe -t secret-subcmds 'secret commands' \
|
||||
'add:Create/open secrets/<name>.txt in $EDITOR'
|
||||
return
|
||||
fi
|
||||
|
||||
local sub
|
||||
sub="${words[3]}"
|
||||
case "$sub" in
|
||||
add)
|
||||
if (( CURRENT == 4 )); then
|
||||
_dcsctl__projects
|
||||
return
|
||||
fi
|
||||
if (( CURRENT == 5 )); then
|
||||
_dcsctl__secret_names_from_dir && return
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
service)
|
||||
if (( CURRENT == 3 )); then
|
||||
local -a service_subcmds
|
||||
service_subcmds=(
|
||||
'add:Add a new service to a swarm project'
|
||||
'ls:List services in a swarm project'
|
||||
)
|
||||
_describe -t service-subcmds 'service commands' service_subcmds
|
||||
return
|
||||
fi
|
||||
|
||||
local sub
|
||||
sub="${words[3]}"
|
||||
case "$sub" in
|
||||
add)
|
||||
if (( CURRENT == 4 )); then
|
||||
_dcsctl__projects
|
||||
return
|
||||
fi
|
||||
# CURRENT == 5: free-form service name, no completions needed
|
||||
;;
|
||||
ls)
|
||||
if (( CURRENT == 4 )); then
|
||||
_dcsctl__projects
|
||||
return
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
import)
|
||||
_arguments -C \
|
||||
'--env-file[Additional env file to ingest]:env file:_files' \
|
||||
'--strict[Fail if compose violates template contract after import]' \
|
||||
'--swarm[Import as swarm project, splitting services into per-service fragments]' \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
'2:compose path:_files' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
context)
|
||||
if (( CURRENT == 3 )); then
|
||||
_describe -t context-subcmds 'context commands' \
|
||||
'init:Initialize ~/.dcs/<context> with env.system and compose/'
|
||||
return
|
||||
fi
|
||||
|
||||
local sub
|
||||
sub="${words[3]}"
|
||||
case "$sub" in
|
||||
init)
|
||||
if (( CURRENT == 4 )); then
|
||||
_dcsctl__docker_contexts
|
||||
return
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
reload)
|
||||
_arguments -C \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
rename)
|
||||
_arguments -C \
|
||||
'1:project name:_dcsctl__projects' \
|
||||
'2:new project name:' \
|
||||
&& return
|
||||
;;
|
||||
|
||||
help)
|
||||
if (( CURRENT == 3 )); then
|
||||
_describe -t commands 'dcsctl commands' subcmds
|
||||
return
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
_describe -t commands 'dcsctl commands' subcmds
|
||||
}
|
||||
|
||||
_dcsctl "$@"
|
||||
Reference in New Issue
Block a user