forked from GreedyBear-Project/GreedyBear
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgbctl
More file actions
executable file
·1239 lines (1082 loc) · 43 KB
/
gbctl
File metadata and controls
executable file
·1239 lines (1082 loc) · 43 KB
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
#!/usr/bin/env bash
# GreedyBear Setup and Management Script
# This script simplifies deployment, updates, and management of GreedyBear instances
set -euo pipefail
# Resolve script directory for absolute path usage
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Version and defaults
GREEDYBEAR_VERSION="latest"
PROJECT_NAME="greedybear"
SILENT_MODE=false
ELASTIC_ENDPOINT=""
# Admin creation defaults
ADMIN_USERNAME="${GB_ADMIN_USERNAME:-}"
ADMIN_PASSWORD="${GB_ADMIN_PASSWORD:-}"
ADMIN_EMAIL="${GB_ADMIN_EMAIL:-}"
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Environment and command validation arrays
declare -A cmd_arguments=(["init"]=1 ["up"]=1 ["start"]=1 ["down"]=1 ["stop"]=1 ["restart"]=1 ["logs"]=1 ["ps"]=1 ["update"]=1 ["build"]=1 ["pull"]=1 ["backup"]=1 ["restore"]=1 ["health"]=1 ["clean"]=1 ["create-admin"]=1)
# Logging functions
log_info() {
if [ "$SILENT_MODE" = false ]; then
echo -e "${BLUE}[INFO]${NC} $1"
fi
}
log_success() {
if [ "$SILENT_MODE" = false ]; then
echo -e "${GREEN}[SUCCESS]${NC} $1"
fi
}
log_warning() {
if [ "$SILENT_MODE" = false ]; then
echo -e "${YELLOW}[WARNING]${NC} $1"
fi
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# Save configuration to .env file
save_config() {
local env_file="${SCRIPT_DIR}/.env"
log_info "Saving configuration to ${env_file}..."
# Build COMPOSE_FILE from flags
local compose_file="docker/default.yml"
[ "$ENV_MODE" = "dev" ] && compose_file+=":docker/local.override.yml"
[ "$ENABLE_HTTPS" = true ] && compose_file+=":docker/https.override.yml"
[ "$ENABLE_ELASTIC" = true ] && compose_file+=":docker/elasticsearch.yml"
[ "$USE_VERSION" = true ] && compose_file+=":docker/version.override.yml"
cat > "$env_file" <<EOF
# GreedyBear configuration (generated by gbctl init)
COMPOSE_PROJECT_NAME=${PROJECT_NAME}
COMPOSE_FILE=${compose_file}
EOF
if [ "$USE_VERSION" = true ]; then
echo "REACT_APP_INTELOWL_VERSION=${GREEDYBEAR_VERSION}" >> "$env_file"
fi
log_success "Configuration saved"
}
# Load configuration from .env file
load_config() {
local env_file="${SCRIPT_DIR}/.env"
if [ -f "$env_file" ]; then
if [ "$SILENT_MODE" = false ] && [ "$COMMAND" != "init" ]; then
log_info "Loading configuration from ${env_file}..."
fi
local compose_file=""
local version_pin=""
# Parse KEY=VALUE pairs
while IFS='=' read -r key value; do
[[ -z "$key" || "$key" == \#* ]] && continue
key="${key#"${key%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
value="${value%\"}"
value="${value#\"}"
case "$key" in
COMPOSE_FILE) compose_file="$value" ;;
REACT_APP_INTELOWL_VERSION) version_pin="$value" ;;
esac
done < "$env_file"
# Derive gbctl flags from COMPOSE_FILE
if [ -n "$compose_file" ]; then
[[ "$compose_file" == *"local.override.yml"* ]] && ENV_MODE="dev" || ENV_MODE="prod"
[[ "$compose_file" == *"https.override.yml"* ]] && ENABLE_HTTPS=true
[[ "$compose_file" == *"elasticsearch.yml"* ]] && ENABLE_ELASTIC=true
if [[ "$compose_file" == *"version.override.yml"* ]]; then
USE_VERSION=true
GREEDYBEAR_VERSION="${version_pin:-latest}"
fi
fi
fi
}
# Print help message
print_help() {
cat << EOF
GreedyBear - Automated Setup and Management Script
SYNOPSIS
./gbctl <command> [OPTIONS]
./gbctl -h|--help
COMMANDS
init Initialize GreedyBear (setup environment files)
up Start all services
start Alias for 'up'
down Stop and remove all services
stop Stop all services without removing them
restart Restart all services
logs View logs from services
ps List running services
update Update GreedyBear to latest version
build Build Docker images
pull Pull latest Docker images
backup Backup database and volumes
restore Restore from backup
health Health check all services
clean Remove all data and reset (destructive!)
create-admin Create a Django superuser (interactive or silent)
GLOBAL OPTIONS
-h, --help Show this help message
-s, --silent Silent mode (non-interactive)
INIT OPTIONS (./gbctl init)
--dev Development deployment with hot-reload
--release <tag> Pin a specific image tag (e.g. stag, 3.0.1)
--https Enable HTTPS with custom certificates
--elastic-local Enable local Elasticsearch (requires >=16GB RAM)
--elastic-endpoint <url> Specify Elasticsearch endpoint
Without --dev or --release, production mode with the "prod" image tag is used.
CLEAN OPTIONS (./gbctl clean)
--force Required to confirm destructive operation
CREATE-ADMIN OPTIONS (./gbctl create-admin)
--username <name> Username (or use GB_ADMIN_USERNAME env var)
--password <pass> Password (or use GB_ADMIN_PASSWORD env var)
--email <addr> Email address (or use GB_ADMIN_EMAIL env var)
LOGS OPTIONS
./gbctl logs View container logs (stdout/stderr)
./gbctl logs app View Django application logs inside container
EXAMPLES
# Initialize production deployment with external TPOT Elasticsearch
./gbctl init --elastic-endpoint http://tpot-host:64298
# Initialize for development with a dummy Elasticsearch instance
./gbctl init --dev --elastic-local
# Initialize staging deployment
./gbctl init --release stag
# Start services (uses configuration from init)
./gbctl up
# View logs
./gbctl logs
# Create admin user
./gbctl create-admin --username admin --password secret --email admin@example.com
# Update to latest version
./gbctl update
# Silent initialization
./gbctl init --silent
EOF
}
# Check if Docker is installed
check_docker() {
log_info "Checking Docker installation..."
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed."
if [ "$SILENT_MODE" = false ]; then
read -p "Would you like to install Docker? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
install_docker
else
log_error "Docker is required to run GreedyBear. Exiting."
exit 1
fi
else
log_error "Please install Docker first. Visit: https://docs.docker.com/get-docker/"
exit 1
fi
else
DOCKER_VERSION=$(docker --version | cut -d ' ' -f3 | tr -d ',')
log_success "Docker ${DOCKER_VERSION} is installed"
fi
}
# Check if Docker Compose is installed
check_docker_compose() {
log_info "Checking Docker Compose installation..."
detect_docker_cmd
if ! ${DOCKER_CMD} compose version &> /dev/null; then
log_error "Docker Compose v2+ is not available."
if [ "$SILENT_MODE" = false ]; then
read -p "Would you like installation instructions? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Please install Docker Compose v2+: https://docs.docker.com/compose/install/"
fi
fi
exit 1
else
COMPOSE_VERSION=$(${DOCKER_CMD} compose version --short)
log_success "Docker Compose ${COMPOSE_VERSION} is installed"
fi
}
# Detect whether docker needs sudo
detect_docker_cmd() {
DOCKER_CMD="docker"
if ! docker ps >/dev/null 2>&1; then
# Try passwordless sudo first
if sudo -n docker ps >/dev/null 2>&1; then
DOCKER_CMD="sudo docker"
else
# Check if silent mode is enabled before prompting for sudo
if [ "$SILENT_MODE" = true ]; then
log_error "Docker requires sudo, but passwordless sudo is not available."
log_error "In --silent mode, you must have passwordless sudo permission or be in the docker group."
exit 1
fi
# Fall back to interactive sudo (may prompt for password)
if sudo docker ps >/dev/null 2>&1; then
DOCKER_CMD="sudo docker"
else
log_error "Docker is not accessible. Ensure you have permission to run Docker commands (with or without sudo)."
exit 1
fi
fi
fi
}
# Install Docker (support for multiple distros)
install_docker() {
log_info "Installing Docker..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case $ID in
ubuntu|debian)
# Use official Docker repository instead of convenience script for security
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/${ID}/gpg" | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker "$USER"
log_success "Docker installed. Please log out and back in for group changes to take effect."
;;
fedora)
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker "$USER"
log_success "Docker installed. Please log out and back in for group changes to take effect."
;;
centos|rhel|almalinux|rocky)
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker "$USER"
log_success "Docker installed. Please log out and back in for group changes to take effect."
;;
opensuse*|sles)
sudo zypper install -y docker docker-compose
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker "$USER"
log_success "Docker installed. Please log out and back in for group changes to take effect."
;;
*)
log_error "Automatic installation not supported for $ID"
log_info "Please install Docker manually:"
log_info " Visit: https://docs.docker.com/get-docker/"
exit 1
;;
esac
else
log_error "Cannot detect OS. Please install Docker manually."
log_info "Visit: https://docs.docker.com/get-docker/"
exit 1
fi
}
# Check system requirements
check_requirements() {
log_info "Checking system requirements..."
# Check available memory
if command -v free &> /dev/null; then
TOTAL_MEM=$(free -g | awk '/^Mem:/{print $2}')
if [ "$ENABLE_ELASTIC" = true ] && [ "$TOTAL_MEM" -lt 16 ]; then
log_warning "Elasticsearch requires at least 16GB RAM. You have ${TOTAL_MEM}GB."
if [ "$SILENT_MODE" = false ]; then
read -p "Continue anyway? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
fi
fi
check_docker
check_docker_compose
# Check if git is installed
if ! command -v git &> /dev/null; then
log_warning "Git is not installed."
log_info "Some features (like update) will not work."
fi
log_success "All requirements met"
}
# Initialize environment files
init_env_files() {
log_info "Initializing environment files..."
# Handle env_file
if [ -f "${SCRIPT_DIR}/docker/env_file" ]; then
if [ "$SILENT_MODE" = true ]; then
# Always create backup even in silent mode to protect user configs
local backup_name="${SCRIPT_DIR}/docker/env_file.backup.$(date +%Y%m%d_%H%M%S)"
cp "${SCRIPT_DIR}/docker/env_file" "$backup_name"
log_info "Backed up existing docker/env_file to $backup_name"
cp "${SCRIPT_DIR}/docker/env_file_template" "${SCRIPT_DIR}/docker/env_file"
log_success "Regenerated docker/env_file from template"
else
log_warning "docker/env_file already exists"
read -p "Do you want to regenerate it? This will backup the existing file. [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
mv "${SCRIPT_DIR}/docker/env_file" "${SCRIPT_DIR}/docker/env_file.backup.$(date +%Y%m%d_%H%M%S)"
cp "${SCRIPT_DIR}/docker/env_file_template" "${SCRIPT_DIR}/docker/env_file"
log_success "Backed up old file and created new docker/env_file from template"
else
log_info "Keeping existing docker/env_file"
fi
fi
else
if [ -f "${SCRIPT_DIR}/docker/env_file_template" ]; then
cp "${SCRIPT_DIR}/docker/env_file_template" "${SCRIPT_DIR}/docker/env_file"
log_success "Created docker/env_file from template"
if [ "$SILENT_MODE" = false ] && [ -z "$ELASTIC_ENDPOINT" ]; then
log_warning "Please edit docker/env_file and configure required settings"
fi
else
log_error "docker/env_file_template not found"
exit 1
fi
fi
# Set Elasticsearch endpoint if provided
if [ -n "$ELASTIC_ENDPOINT" ]; then
# Escape ampersand to avoid sed interpreting it as the matched string
local escaped_endpoint="${ELASTIC_ENDPOINT//&/\\&}"
if grep -q "^ELASTIC_ENDPOINT=" "${SCRIPT_DIR}/docker/env_file"; then
sed -i "s|^ELASTIC_ENDPOINT=.*|ELASTIC_ENDPOINT=${escaped_endpoint}|" "${SCRIPT_DIR}/docker/env_file"
else
echo "ELASTIC_ENDPOINT=${ELASTIC_ENDPOINT}" >> "${SCRIPT_DIR}/docker/env_file"
fi
log_success "Configured ELASTIC_ENDPOINT in docker/env_file"
fi
# Handle env_file_postgres
if [ -f "${SCRIPT_DIR}/docker/env_file_postgres" ]; then
if [ "$SILENT_MODE" = true ]; then
# Always create backup even in silent mode to protect user configs
local backup_name="${SCRIPT_DIR}/docker/env_file_postgres.backup.$(date +%Y%m%d_%H%M%S)"
cp "${SCRIPT_DIR}/docker/env_file_postgres" "$backup_name"
log_info "Backed up existing docker/env_file_postgres to $backup_name"
cp "${SCRIPT_DIR}/docker/env_file_postgres_template" "${SCRIPT_DIR}/docker/env_file_postgres"
log_success "Regenerated docker/env_file_postgres from template"
else
log_warning "docker/env_file_postgres already exists"
read -p "Do you want to regenerate it? This will backup the existing file. [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
mv "${SCRIPT_DIR}/docker/env_file_postgres" "${SCRIPT_DIR}/docker/env_file_postgres.backup.$(date +%Y%m%d_%H%M%S)"
cp "${SCRIPT_DIR}/docker/env_file_postgres_template" "${SCRIPT_DIR}/docker/env_file_postgres"
log_success "Backed up old file and created new docker/env_file_postgres from template"
else
log_info "Keeping existing docker/env_file_postgres"
fi
fi
else
if [ -f "${SCRIPT_DIR}/docker/env_file_postgres_template" ]; then
cp "${SCRIPT_DIR}/docker/env_file_postgres_template" "${SCRIPT_DIR}/docker/env_file_postgres"
log_success "Created docker/env_file_postgres from template"
else
log_error "docker/env_file_postgres_template not found"
exit 1
fi
fi
log_success "Environment files initialized"
# Save .env configuration (COMPOSE_FILE, COMPOSE_PROJECT_NAME, etc.)
if [ -f "${SCRIPT_DIR}/.env" ]; then
if [ "$SILENT_MODE" = false ]; then
log_warning ".env already exists"
read -p "Do you want to regenerate it? This will backup the existing file. [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
mv "${SCRIPT_DIR}/.env" "${SCRIPT_DIR}/.env.backup.$(date +%Y%m%d_%H%M%S)"
save_config
else
log_info "Keeping existing .env"
fi
else
local backup_name="${SCRIPT_DIR}/.env.backup.$(date +%Y%m%d_%H%M%S)"
cp "${SCRIPT_DIR}/.env" "$backup_name"
log_info "Backed up existing .env to $backup_name"
save_config
fi
else
save_config
fi
}
# Execute docker compose command
# --project-directory points to docker/ so that relative env_file paths
# in compose files resolve correctly. --env-file loads .env from the
# repo root for COMPOSE_FILE, COMPOSE_PROJECT_NAME, etc.
execute_compose() {
local cmd=$1
shift
log_info "Executing: docker compose $cmd $*"
detect_docker_cmd
${DOCKER_CMD} compose --project-directory "${SCRIPT_DIR}/docker" \
--env-file "${SCRIPT_DIR}/.env" "$cmd" "$@"
}
# Checkout the appropriate git branch/tag for the deployment mode
checkout_git_ref() {
if [ ! -d "${SCRIPT_DIR}/.git" ]; then
return
fi
if ! command -v git &> /dev/null; then
return
fi
local target_ref="main"
if [ "$ENV_MODE" = "dev" ]; then
target_ref="develop"
elif [ "$USE_VERSION" = true ]; then
if [ "$GREEDYBEAR_VERSION" = "stag" ]; then
target_ref="develop"
else
target_ref="$GREEDYBEAR_VERSION"
fi
fi
local current_ref
current_ref=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || git -C "$SCRIPT_DIR" rev-parse --short HEAD)
if [ "$current_ref" = "$target_ref" ]; then
log_info "Already on '$target_ref'"
return
fi
if [ -n "$(git -C "$SCRIPT_DIR" status --porcelain)" ]; then
if [ "$SILENT_MODE" = true ]; then
log_warning "Uncommitted changes detected. Force checking out '$target_ref'..."
else
log_warning "You have uncommitted changes in the repository."
read -r -p "Continue checking out '$target_ref'? Uncommitted changes may be lost. [y/N] " response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
log_info "Checkout aborted."
exit 0
fi
fi
fi
log_info "Checking out '$target_ref'..."
if git -C "$SCRIPT_DIR" checkout "$target_ref"; then
log_success "Checked out '$target_ref'"
else
log_error "Failed to checkout '$target_ref'. Please check your git state."
exit 1
fi
}
# Initialize GreedyBear
cmd_init() {
log_info "Initializing GreedyBear..."
check_requirements
checkout_git_ref
init_env_files
log_success "Initialization complete!"
echo
log_info "Next steps:"
if [ "$SILENT_MODE" = false ]; then
log_info " 1. Review and configure docker/env_file with your settings"
log_info " 2. Start GreedyBear with: ./gbctl up"
else
log_info " Start GreedyBear with: ./gbctl up"
fi
}
# Check for version downgrade
check_downgrade() {
# Only check if a specific version is requested
if [ "$USE_VERSION" != true ]; then
return
fi
detect_docker_cmd
local docker_cmd="$DOCKER_CMD"
# Check if gunicorn container exists
local container_name="${PROJECT_NAME}_app"
if ! ${docker_cmd} ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
return
fi
# Get current image
local current_image
current_image=$(${docker_cmd} inspect --format='{{.Config.Image}}' "$container_name" 2>/dev/null || echo "")
if [ -z "$current_image" ]; then
return
fi
# Extract tag (part after :)
local current_tag="${current_image##*:}"
# If current tag is "prod", "latest" or matches requested, skip
# We can't reliably compare "prod"/"latest" with semantic versions
if [ "$current_tag" = "prod" ] || [ "$current_tag" = "latest" ] || [ "$current_tag" = "$GREEDYBEAR_VERSION" ]; then
return
fi
# Check for downgrade using sort -V
local lowest_version
lowest_version=$(echo -e "${current_tag}\n${GREEDYBEAR_VERSION}" | sort -V | head -n1)
if [ "$lowest_version" = "$GREEDYBEAR_VERSION" ] && [ "$current_tag" != "$GREEDYBEAR_VERSION" ]; then
log_warning "POTENTIAL DOWNGRADE DETECTED!"
log_warning "Current running version: $current_tag"
log_warning "Target version: $GREEDYBEAR_VERSION"
log_warning "Downgrading may cause database incompatibility issues since the database"
log_warning "might have been migrated to the newer version."
if [ "$SILENT_MODE" = false ]; then
read -p "Are you sure you want to proceed? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Operation cancelled"
exit 1
fi
else
log_error "Aborting downgrade in silent mode to prevent data corruption."
exit 1
fi
fi
}
# Start services
cmd_up() {
log_info "Starting GreedyBear services..."
check_downgrade
check_git_version_mismatch
execute_compose up -d "$@"
log_success "GreedyBear services started successfully!"
}
# Check for git version mismatch
check_git_version_mismatch() {
# Only check if we are in a git repo and not pinning a specific version
if [ -d "${SCRIPT_DIR}/.git" ] && [ "$USE_VERSION" != true ]; then
if command -v git &> /dev/null; then
local current_branch
current_branch=$(git -C "$SCRIPT_DIR" branch --show-current)
# If we are on main/master but running dev, or vice versa
# This is a heuristic check
if [ "$ENV_MODE" = "prod" ] && [ "$current_branch" != "main" ] && [ "$current_branch" != "master" ]; then
log_warning "Version Mismatch Warning:"
log_warning " You are running in PROD mode but your git branch is '$current_branch'."
log_warning " Production usually runs on 'main' or 'master'."
elif [ "$ENV_MODE" = "dev" ] && ([ "$current_branch" = "main" ] || [ "$current_branch" = "master" ]); then
log_info "Note: You are running in DEV mode on the '$current_branch' branch."
fi
fi
fi
}
# Stop and remove services
cmd_down() {
log_info "Stopping GreedyBear services..."
execute_compose down "$@"
log_success "GreedyBear services stopped"
}
# Stop services
cmd_stop() {
log_info "Stopping GreedyBear services..."
execute_compose stop "$@"
log_success "GreedyBear services stopped"
}
# Restart services
cmd_restart() {
log_info "Restarting GreedyBear services..."
execute_compose restart "$@"
log_success "GreedyBear services restarted"
}
# View logs
cmd_logs() {
if [ $# -gt 0 ] && [ "$1" == "app" ]; then
shift
log_info "Tailing Django application logs (Ctrl+C to exit)..."
detect_docker_cmd
local container_name="${PROJECT_NAME}_app"
# Check if container is running
if ! ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
log_error "Container $container_name is not running."
exit 1
fi
# Tail the log file inside the container
${DOCKER_CMD} exec -it "$container_name" tail -f /var/log/greedybear/django/greedybear.log
else
execute_compose logs "$@"
fi
}
# List services
cmd_ps() {
execute_compose ps "$@"
}
# Update GreedyBear
cmd_update() {
log_info "Updating GreedyBear..."
if [ -d "${SCRIPT_DIR}/.git" ]; then
if [ "$USE_VERSION" = true ] && [ "$GREEDYBEAR_VERSION" != "stag" ]; then
# Pinned to a version tag — don't pull, just check for newer releases
git -C "$SCRIPT_DIR" fetch --tags --quiet 2>/dev/null
local latest_tag
latest_tag=$(git -C "$SCRIPT_DIR" tag | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
if [ -n "$latest_tag" ] && [ "$latest_tag" != "$GREEDYBEAR_VERSION" ]; then
log_warning "Pinned to ${GREEDYBEAR_VERSION}, but ${latest_tag} is available"
log_info "To upgrade, run: ./gbctl init --release ${latest_tag}"
fi
else
# Tracking a branch (develop, main, or stag) — pull latest
log_info "Pulling latest code from git..."
if git -C "$SCRIPT_DIR" pull; then
log_success "Code updated successfully"
else
log_error "Failed to pull latest code. Please check your git configuration."
exit 1
fi
fi
else
log_warning "Not a git repository. Skipping code update."
fi
# Pull latest images
log_info "Pulling latest Docker images..."
execute_compose pull
# Stop existing services and remove orphaned containers from removed services
log_info "Stopping existing services..."
execute_compose down --remove-orphans
# Start services using cmd_up to ensure version env vars are applied
log_info "Starting services with new images..."
cmd_up "$@"
log_success "GreedyBear updated successfully!"
}
# Build images
cmd_build() {
log_info "Building GreedyBear images..."
execute_compose build "$@"
log_success "Build complete"
}
# Pull images
cmd_pull() {
log_info "Pulling GreedyBear images..."
execute_compose pull "$@"
log_success "Pull complete"
}
# Backup PostgreSQL database
cmd_backup() {
log_info "Creating backup..."
local backup_dir="${SCRIPT_DIR}/backups"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="${backup_dir}/greedybear_backup_${timestamp}"
# Securely create backup directory
if [ ! -d "$backup_dir" ]; then
mkdir -p "$backup_dir"
chmod 700 "$backup_dir"
fi
# Source PostgreSQL credentials from env file
local pg_user="user"
local pg_db="greedybear_db"
if [ -f "${SCRIPT_DIR}/docker/env_file_postgres" ]; then
pg_user=$(grep -E '^POSTGRES_USER=' "${SCRIPT_DIR}/docker/env_file_postgres" | cut -d'=' -f2 || echo "user")
pg_db=$(grep -E '^POSTGRES_DB=' "${SCRIPT_DIR}/docker/env_file_postgres" | cut -d'=' -f2 || echo "greedybear_db")
fi
detect_docker_cmd
local docker_cmd="$DOCKER_CMD"
# Check if postgres container is running
if ! ${docker_cmd} ps --format '{{.Names}}' | grep -q "${PROJECT_NAME}_postgres"; then
log_error "PostgreSQL container is not running. Start services first."
exit 1
fi
# Backup PostgreSQL database
# Write dump to a file inside the container to avoid stdout corruption,
# then copy it out to the host.
log_info "Backing up PostgreSQL database..."
set +e # Temporarily disable exit on error to handle failures gracefully
${docker_cmd} exec ${PROJECT_NAME}_postgres pg_dump -U "$pg_user" -f /tmp/gb_backup.sql "$pg_db"
backup_status=$?
set -e # Re-enable exit on error
if [ $backup_status -eq 0 ]; then
${docker_cmd} cp ${PROJECT_NAME}_postgres:/tmp/gb_backup.sql "${backup_file}.sql"
${docker_cmd} exec ${PROJECT_NAME}_postgres rm -f /tmp/gb_backup.sql
gzip "${backup_file}.sql"
chmod 600 "${backup_file}.sql.gz"
log_success "Database backup created: ${backup_file}.sql.gz"
else
${docker_cmd} exec ${PROJECT_NAME}_postgres rm -f /tmp/gb_backup.sql 2>/dev/null || true
log_error "Database backup failed"
exit 1
fi
# Backup volumes info
log_info "Saving volumes information..."
${docker_cmd} volume ls --filter name=${PROJECT_NAME} > "${backup_file}_volumes.txt"
log_success "Backup complete: ${backup_file}.sql.gz"
log_info "To restore: ./gbctl restore ${backup_file}.sql.gz"
}
# Restore from backup
cmd_restore() {
if [ $# -eq 0 ]; then
log_error "Please specify backup file to restore"
log_info "Usage: ./gbctl restore backups/greedybear_backup_YYYYMMDD_HHMMSS.sql.gz"
log_info "Available backups in ${SCRIPT_DIR}/backups:"
ls -lh "${SCRIPT_DIR}/backups/"*.sql.gz 2>/dev/null || log_warning "No backups found"
exit 1
fi
local backup_file=$1
if [ ! -f "$backup_file" ]; then
log_error "Backup file not found: $backup_file"
exit 1
fi
detect_docker_cmd
local docker_cmd="$DOCKER_CMD"
# Source PostgreSQL credentials from env file
local pg_user="user"
local pg_db="greedybear_db"
if [ -f "${SCRIPT_DIR}/docker/env_file_postgres" ]; then
pg_user=$(grep -E '^POSTGRES_USER=' "${SCRIPT_DIR}/docker/env_file_postgres" | cut -d'=' -f2 || echo "user")
pg_db=$(grep -E '^POSTGRES_DB=' "${SCRIPT_DIR}/docker/env_file_postgres" | cut -d'=' -f2 || echo "greedybear_db")
fi
# Check if postgres container is running
if ! ${docker_cmd} ps --format '{{.Names}}' | grep -q "${PROJECT_NAME}_postgres"; then
log_error "PostgreSQL container is not running. Start services first."
exit 1
fi
if [ "$SILENT_MODE" = false ]; then
log_warning "This will overwrite the current database!"
read -p "Are you sure you want to restore from backup? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Restore cancelled"
exit 0
fi
fi
log_info "Restoring database from: $backup_file"
# Drop and recreate the database to avoid duplicate object errors
log_info "Dropping and recreating database..."
${docker_cmd} exec ${PROJECT_NAME}_postgres dropdb -U "$pg_user" "$pg_db"
${docker_cmd} exec ${PROJECT_NAME}_postgres createdb -U "$pg_user" "$pg_db"
# Decompress and restore
set +e # Temporarily disable exit on error to handle failures gracefully
if [[ $backup_file == *.gz ]]; then
# Use explicit pipefail check for gunzip | docker exec pipeline
set -o pipefail
gunzip -c "$backup_file" | ${docker_cmd} exec -i ${PROJECT_NAME}_postgres psql -U "$pg_user" "$pg_db"
restore_status=$?
set -o pipefail # Keep pipefail enabled (script default)
else
${docker_cmd} exec -i ${PROJECT_NAME}_postgres psql -U "$pg_user" "$pg_db" < "$backup_file"
restore_status=$?
fi
set -e # Re-enable exit on error
if [ $restore_status -eq 0 ]; then
log_success "Database restored successfully"
else
log_error "Restore failed"
exit 1
fi
}
# Health check all services
cmd_health() {
log_info "Checking GreedyBear services health..."
echo
detect_docker_cmd
local docker_cmd="$DOCKER_CMD"
# Check each service
local services=("postgres" "app" "nginx" "qcluster")
local all_healthy=true
for service in "${services[@]}"; do
local container_name="${PROJECT_NAME}_${service}"
if ${docker_cmd} ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
local status=$(${docker_cmd} inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
local health=$(${docker_cmd} inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}no healthcheck{{end}}' "$container_name" 2>/dev/null)
if [ "$status" = "running" ]; then
if [ "$health" = "healthy" ] || [ "$health" = "no healthcheck" ]; then
echo -e "${GREEN}✓${NC} $service: running"
else
echo -e "${YELLOW}!${NC} $service: running (health: $health)"
all_healthy=false
fi
else
echo -e "${RED}✗${NC} $service: $status"
all_healthy=false
fi
else
echo -e "${RED}✗${NC} $service: not running"
all_healthy=false
fi
done
echo
if [ "$all_healthy" = true ]; then
log_success "All services are healthy"
return 0
else
log_warning "Some services have issues"
log_info "Run './gbctl logs <service>' for details"
return 1
fi
}
# Clean all data and reset
cmd_clean() {
log_warning "This will remove ALL GreedyBear data, containers, and volumes!"
log_warning "This action is IRREVERSIBLE!"
# Always require explicit --force flag for safety
if [ "$FORCE_CLEAN" = false ]; then
log_error "The clean command requires the --force flag to prevent accidental data loss"
log_info "Usage: ./gbctl clean --force"
exit 1
fi
if [ "$SILENT_MODE" = false ]; then
read -p "Type 'yes' to confirm complete removal: " confirmation
if [ "$confirmation" != "yes" ]; then
log_info "Clean cancelled"
exit 0
fi
fi
log_info "Stopping and removing all containers..."
execute_compose down -v
log_info "Removing environment and configuration files..."
rm -f "${SCRIPT_DIR}/docker/env_file" "${SCRIPT_DIR}/docker/env_file_postgres" "${SCRIPT_DIR}/.env"
log_success "GreedyBear has been completely removed"
log_info "To reinstall, run: ./gbctl init"
}
# Create admin user
cmd_create_admin() {
log_info "Creating Django superuser..."
detect_docker_cmd
local docker_cmd="$DOCKER_CMD"
local container_name="${PROJECT_NAME}_app"
# Check if app container is running
if ! ${docker_cmd} ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
log_error "GreedyBear Gunicorn container is not running. Please start services first."
exit 1
fi
# Check if we have arguments for silent mode (from flags or environment variables)
if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_PASSWORD" ] && [ -n "$ADMIN_EMAIL" ]; then
log_info "Creating superuser in non-interactive mode..."
if ${docker_cmd} exec -e DJANGO_SUPERUSER_PASSWORD="$ADMIN_PASSWORD" \
-e DJANGO_SUPERUSER_USERNAME="$ADMIN_USERNAME" \
-e DJANGO_SUPERUSER_EMAIL="$ADMIN_EMAIL" \
-e DJANGO_SUPERUSER_FIRST_NAME="admin" \
-e DJANGO_SUPERUSER_LAST_NAME="user" \
"$container_name" python3 manage.py createsuperuser --noinput; then
log_success "Superuser '$ADMIN_USERNAME' created successfully!"
else
log_error "Failed to create superuser."
exit 1
fi
else
# Fallback to interactive mode if ANY argument is missing
if [ "$SILENT_MODE" = true ] && ( [ -z "$ADMIN_USERNAME" ] || [ -z "$ADMIN_PASSWORD" ] || [ -z "$ADMIN_EMAIL" ] ); then
log_error "Silent mode requires username, password, and email."
log_info "Provide them via flags (--username, --password, --email)"
log_info "OR via environment variables (GB_ADMIN_USERNAME, GB_ADMIN_PASSWORD, GB_ADMIN_EMAIL)"
exit 1
fi
log_info "Entering interactive mode..."
log_info "Please follow the prompts to create your superuser."
# Use -it for interactive terminal