-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
Β·1780 lines (1567 loc) Β· 72.8 KB
/
deploy.sh
File metadata and controls
executable file
Β·1780 lines (1567 loc) Β· 72.8 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
#!/bin/bash
# WordPress Enterprise Deployment Script
# Production-ready WordPress with enhanced security and upgrade support
# Version: 3.1.0 - Added instance detection and safe upgrade functionality
# Function definitions first
show_credentials_only() {
local namespace="wordpress"
local release="wordpress"
if [[ -n "${2:-}" ]]; then
namespace="$2"
fi
if [[ -n "${3:-}" ]]; then
release="$3"
fi
echo "π WordPress Database Credentials (from Kubernetes secret)"
echo "=========================================================="
echo
if ! kubectl get secret "$release" -n "$namespace" &>/dev/null; then
echo "β Secret '$release' not found in namespace '$namespace'"
echo "π‘ Usage: $0 --show-credentials [namespace] [release-name]"
exit 1
fi
echo "Database (MariaDB):"
echo " Root Password: $(kubectl get secret "$release" -n "$namespace" -o jsonpath='{.data.mariadb-root-password}' | base64 -d)"
echo " WordPress Password: $(kubectl get secret "$release" -n "$namespace" -o jsonpath='{.data.mariadb-password}' | base64 -d)"
echo
echo "Redis Cache:"
echo " Password: $(kubectl get secret "$release" -n "$namespace" -o jsonpath='{.data.redis-password}' | base64 -d)"
echo
echo "π WordPress URL: https://$(kubectl get ingress -n "$namespace" -o jsonpath='{.items[0].spec.rules[0].host}' 2>/dev/null || echo 'Check ingress configuration')"
echo
echo "βΉοΈ WordPress admin credentials are set up during post-deployment installation wizard"
echo " Visit your WordPress URL and follow the installation steps to create admin account"
echo
echo "β οΈ Keep these database credentials secure and private!"
}
# Instance detection and management functions
detect_existing_instance() {
local namespace="$1"
local release="$2"
# Check for any helm release (deployed, failed, etc.)
if helm list -n "$namespace" 2>/dev/null | grep -q "$release"; then
return 0 # Instance exists (any status)
fi
# Also check for existing PVCs (data exists even without Helm release)
if kubectl get pvc -n "$namespace" 2>/dev/null | grep -q "wordpress\|mariadb"; then
return 0 # Existing data found
fi
return 1 # No instance found
}
show_instance_detected() {
local namespace="$1"
local release="$2"
echo
log_step "Instance Detection Results"
echo "π Existing WordPress instance detected!"
echo " Namespace: $namespace"
echo " Release: $release"
echo " Helm Status: $(helm status "$release" -n "$namespace" -o json 2>/dev/null | grep -o '"status":"[^"]*"' | cut -d'"' -f4 || echo 'no release (data-only)')"
echo " Data: $(kubectl get pvc -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ') PVCs found"
echo " Pods: $(kubectl get pods -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ') running"
echo
}
prompt_deployment_choice() {
echo "Choose deployment option:" >&2
echo " 1) π Update existing instance (recommended - preserves data)" >&2
echo " 2) π Deploy new instance with different name" >&2
echo " 3) β Cancel deployment" >&2
echo >&2
read -p "Select option [1-3]: " choice >&2
echo "$choice"
}
cleanup_stuck_resources() {
local namespace="$1"
echo "π§Ή Cleaning up stuck resources in namespace: $namespace"
# Remove failed backup jobs
echo " - Removing failed backup jobs..."
kubectl delete job -l app.kubernetes.io/component=backup \
--field-selector=status.successful=0 -n "$namespace" 2>/dev/null || true
# Remove old completed cron jobs (keep last 3)
echo " - Cleaning up old cron job executions..."
# macOS vs Linux date command compatibility
if [[ "$(uname)" == "Darwin" ]]; then
three_days_ago=$(date -v-3d -u +%Y-%m-%dT%H:%M:%SZ)
else
three_days_ago=$(date -d '3 days ago' -u +%Y-%m-%dT%H:%M:%SZ)
fi
kubectl delete job -l app.kubernetes.io/component=cron \
--field-selector=status.completionTime\<$three_days_ago \
-n "$namespace" 2>/dev/null || true
# Remove any pods in Error/Completed state
echo " - Removing stuck pods..."
kubectl delete pods --field-selector=status.phase=Failed -n "$namespace" 2>/dev/null || true
kubectl delete pods --field-selector=status.phase=Succeeded -n "$namespace" 2>/dev/null || true
echo "β
Resource cleanup completed"
}
sync_database_secrets() {
local namespace="$1"
echo "π Synchronizing database secrets..."
# Check if wordpress-mariadb secret exists and has placeholder values
if kubectl get secret wordpress-mariadb -n "$namespace" &>/dev/null; then
local mariadb_pass=$(kubectl get secret wordpress-mariadb -n "$namespace" -o jsonpath='{.data.mariadb-password}' | base64 -d)
if [[ "$mariadb_pass" == *"PLACEHOLDER"* ]]; then
echo " - Fixing MariaDB password synchronization..."
kubectl patch secret wordpress-mariadb -n "$namespace" -p '{
"data": {
"mariadb-password": "'$(kubectl get secret wordpress -n "$namespace" -o jsonpath='{.data.mariadb-password}')'",
"mariadb-root-password": "'$(kubectl get secret wordpress -n "$namespace" -o jsonpath='{.data.mariadb-root-password}')'"
}
}'
echo " β
Database secrets synchronized"
else
echo " β
Database secrets already synchronized"
fi
fi
}
prompt_namespace_and_release() {
echo ""
echo "π¦ Namespace and Release Configuration"
echo "========================================"
echo ""
echo "Choose your deployment namespace and release name:"
echo " 1) Use default (namespace: wordpress, release: wordpress)"
echo " 2) Use custom names"
echo ""
read -p "Select option [1-2]: " ns_choice
echo ""
case $ns_choice in
1)
NAMESPACE="wordpress"
RELEASE_NAME="wordpress"
log_info "Using default namespace and release: wordpress"
;;
2)
while true; do
read -p "Enter custom namespace name: " custom_ns
if [[ "$custom_ns" =~ ^[a-z0-9-]+$ ]] && [[ ${#custom_ns} -le 63 ]]; then
NAMESPACE="$custom_ns"
break
else
echo "β Invalid namespace. Use lowercase letters, numbers, and hyphens only (max 63 chars)"
fi
done
while true; do
read -p "Enter custom release name: " custom_release
if [[ "$custom_release" =~ ^[a-z0-9-]+$ ]] && [[ ${#custom_release} -le 63 ]]; then
RELEASE_NAME="$custom_release"
break
else
echo "β Invalid release name. Use lowercase letters, numbers, and hyphens only (max 63 chars)"
fi
done
log_info "Using custom configuration - Namespace: $NAMESPACE, Release: $RELEASE_NAME"
;;
*)
log_warning "Invalid choice. Using defaults."
NAMESPACE="wordpress"
RELEASE_NAME="wordpress"
;;
esac
echo ""
}
update_existing_instance() {
local namespace="$1"
local release="$2"
local domain="$3"
local email="$4"
echo "π Updating existing WordPress instance..."
echo " Namespace: $namespace"
echo " Release: $release"
echo " Domain: $domain"
echo
# Step 1: Clean up stuck resources
cleanup_stuck_resources "$namespace"
# Step 2: Check and preserve existing TLS certificate
echo "π Checking existing TLS certificate..."
local cert_exists=false
local cert_ready=false
if kubectl get certificate "$release-tls" -n "$namespace" &>/dev/null; then
cert_exists=true
local cert_status=$(kubectl get certificate "$release-tls" -n "$namespace" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null)
if [[ "$cert_status" == "True" ]]; then
cert_ready=true
echo " β
Valid TLS certificate found - will be preserved"
echo " π
Certificate: $(kubectl get certificate "$release-tls" -n "$namespace" -o jsonpath='{.status.notAfter}' 2>/dev/null || echo 'Active')"
else
echo " β οΈ TLS certificate exists but not ready - will attempt renewal"
fi
else
echo " βΉοΈ No existing TLS certificate - will create new one"
fi
# Step 3: Extract existing database credentials (NO new passwords for existing data)
echo "π Extracting existing database credentials from MariaDB data..."
local existing_wp_password=""
local existing_root_password=""
local use_existing_creds=false
# Check if we can find existing credentials in old secrets
if kubectl get secret wordpress-mariadb -n "$namespace" &>/dev/null; then
existing_wp_password=$(kubectl get secret wordpress-mariadb -n "$namespace" -o jsonpath='{.data.mariadb-password}' 2>/dev/null | base64 -d || echo "")
existing_root_password=$(kubectl get secret wordpress-mariadb -n "$namespace" -o jsonpath='{.data.mariadb-root-password}' 2>/dev/null | base64 -d || echo "")
if [[ -n "$existing_wp_password" ]] && [[ -n "$existing_root_password" ]]; then
echo " β
Found existing MariaDB credentials - will reuse them"
use_existing_creds=true
fi
fi
if [[ "$use_existing_creds" == "false" ]]; then
echo " β οΈ Could not extract existing credentials"
echo " βΉοΈ Strategy: Deploy fresh, let MariaDB initialize from existing data"
echo " The existing data will determine the correct credentials"
fi
# Step 4: Deploy fresh WordPress (will connect to existing PVCs automatically)
echo "π¦ Deploying fresh WordPress that will connect to existing data..."
echo "βΉοΈ WordPress will automatically detect and use existing database/content..."
# Skip the infrastructure setup and go straight to WordPress deployment
# Set global variables so the normal deployment flow can use them
DOMAIN="$domain"
FULL_DOMAIN="$domain"
EMAIL="$email"
INCLUDE_WWW=false
# Call the normal deployment functions, conditionally skip infrastructure
log_info "Setting up namespace and secrets for existing data connection..."
# Pass existing credentials to setup function
if [[ "$use_existing_creds" == "true" ]]; then
export EXISTING_MARIADB_PASSWORD="$existing_wp_password"
export EXISTING_MARIADB_ROOT_PASSWORD="$existing_root_password"
echo " βΉοΈ Using extracted existing MariaDB credentials"
fi
setup_namespace_and_secrets
# Skip infrastructure setup if we have a valid certificate (avoids rate limits)
if [[ "$cert_ready" == "true" ]]; then
log_info "Skipping infrastructure setup - using existing TLS certificate"
export SKIP_INFRASTRUCTURE=true
else
log_info "Will set up infrastructure - certificate needs creation/renewal"
export SKIP_INFRASTRUCTURE=false
fi
log_info "Deploying WordPress to connect with existing data..."
deploy_wordpress
echo "β
WordPress deployed and connected to existing data"
# Step 5: Verify deployment is working
echo "π Verifying deployment status..."
# Check pod readiness
if kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=wordpress -n "$namespace" --timeout=120s 2>/dev/null; then
echo "β
WordPress pods are ready"
else
echo "β οΈ WordPress pods initializing (this is normal for fresh deployments)"
echo " - WordPress will be available once health checks pass"
echo " - Database connection may take 1-2 minutes to establish"
fi
# Step 8: Configuration update completed
echo
echo "π WordPress instance update completed!"
echo "π Access your site at: https://$domain"
echo
# Show final certificate status
if kubectl get certificate "$release-tls" -n "$namespace" &>/dev/null; then
local final_cert_status=$(kubectl get certificate "$release-tls" -n "$namespace" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null)
if [[ "$final_cert_status" == "True" ]]; then
echo "π TLS Certificate: β
Active and Valid"
if [[ "$cert_ready" == "true" ]]; then
echo " π Status: Existing certificate preserved (no new issuance)"
else
echo " π Status: New certificate issued successfully"
fi
else
echo "π TLS Certificate: β³ Issuing (check status with: kubectl get certificate -n $namespace)"
fi
fi
echo
echo "π‘ To view credentials: $0 --show-credentials $namespace $release"
return 0
}
show_help() {
echo "WordPress Enterprise Deployment Script v3.1.0"
echo "============================================="
echo
echo "β¨ New Features:"
echo " β’ Automatic instance detection and safe upgrades"
echo " β’ Resource cleanup and configuration synchronization"
echo " β’ Preserves all data during updates"
echo
echo "Usage:"
echo " $0 # Deploy/Update WordPress"
echo " $0 --show-credentials [ns] [release] # Show credentials"
echo " $0 --help # Show this help"
echo
echo "Examples:"
echo " $0 --show-credentials # Default namespace/release"
echo " $0 --show-credentials my-wp my-site # Custom namespace/release"
}
# Check for credential display flag
if [[ "$1" == "--show-credentials" ]]; then
show_credentials_only "$@"
exit 0
fi
if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
show_help
exit 0
fi
# WordPress Enterprise Deployment Script v3.0.0
# Enhanced security, user experience, and production readiness
# Compatible with enterprise zero-trust security standards
set -euo pipefail
# Colors and formatting
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly WHITE='\033[1;37m'
readonly NC='\033[0m' # No Color
readonly BOLD='\033[1m'
# Script metadata
readonly SCRIPT_NAME="WordPress Enterprise Deployment"
readonly SCRIPT_VERSION="3.1.0"
readonly SCRIPT_AUTHOR="WordPress Enterprise"
# Default values - standard WordPress naming
readonly DEFAULT_NAMESPACE="wordpress"
readonly DEFAULT_RELEASE_NAME="wordpress"
readonly HELM_CHART_PATH="./helm"
# State management removed for simplicity and restartability
# Global variables
DOMAIN=""
SUBDOMAIN="wp"
NAMESPACE="${NAMESPACE:-}" # Use environment variable or empty
RELEASE_NAME="${RELEASE_NAME:-}" # Use environment variable or empty
EMAIL=""
DEPLOY_STATE=""
SKIP_PREREQUISITES=false
ENABLE_MONITORING=true
ENABLE_BACKUP=true
SHOW_CREDENTIALS="false" # Default to secure behavior
ENABLE_FLUENT_AUTH_FIX="false" # Disabled by default
# Logging functions
log_info() {
echo -e "${CYAN}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "\n${PURPLE}βΆ${NC} ${BOLD}$1${NC}"
}
log_substep() {
echo -e " ${BLUE}β’${NC} $1"
}
# State management functions removed - deployment is now stateless and restartable
# OS Detection and tool installation functions
detect_os() {
case "$(uname -s)" in
Darwin*) echo "macOS" ;;
Linux*) echo "Linux" ;;
CYGWIN*|MINGW*|MSYS*) echo "Windows" ;;
*) echo "Unknown" ;;
esac
}
get_install_instructions() {
local tool="$1"
local os="$2"
case "$tool" in
"kubectl")
case "$os" in
"macOS") echo "Install via Homebrew: brew install kubectl" ;;
"Linux") echo "curl -LO https://dl.k8s.io/release/\$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" ;;
"Windows") echo "Install via Chocolatey: choco install kubernetes-cli" ;;
*) echo "Visit: https://kubernetes.io/docs/tasks/tools/install-kubectl/" ;;
esac
;;
"helm")
case "$os" in
"macOS") echo "Install via Homebrew: brew install helm" ;;
"Linux") echo "curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash" ;;
"Windows") echo "Install via Chocolatey: choco install kubernetes-helm" ;;
*) echo "Visit: https://helm.sh/docs/intro/install/" ;;
esac
;;
"openssl")
case "$os" in
"macOS") echo "openssl is pre-installed on macOS" ;;
"Linux") echo "sudo apt-get install openssl (Ubuntu)" ;;
"Windows") echo "Use Git Bash or WSL with openssl" ;;
*) echo "Visit: https://www.openssl.org/source/" ;;
esac
;;
"curl")
case "$os" in
"macOS") echo "curl is pre-installed on macOS" ;;
"Linux") echo "sudo apt-get install curl (Ubuntu)" ;;
"Windows") echo "curl is available in Windows 10+" ;;
*) echo "Visit: https://curl.se/download.html" ;;
esac
;;
"git")
case "$os" in
"macOS") echo "brew install git or xcode-select --install" ;;
"Linux") echo "sudo apt-get install git (Ubuntu)" ;;
"Windows") echo "Install Git for Windows: https://git-scm.com/download/win" ;;
*) echo "Visit: https://git-scm.com/downloads" ;;
esac
;;
"argon2")
case "$os" in
"macOS") echo "brew install argon2" ;;
"Linux") echo "sudo apt-get install argon2 (Ubuntu)" ;;
"Windows") echo "Use WSL with: sudo apt-get install argon2" ;;
*) echo "Visit: https://github.com/P-H-C/phc-winner-argon2" ;;
esac
;;
esac
}
# Validation functions
validate_domain() {
local domain="$1"
if [[ ! "$domain" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.[a-zA-Z]{2,}$ ]]; then
log_error "Invalid domain format: $domain"
return 1
fi
return 0
}
validate_email() {
local email="$1"
if [[ ! "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
log_error "Invalid email format: $email"
return 1
fi
return 0
}
validate_namespace() {
local namespace="$1"
if [[ ! "$namespace" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
log_error "Invalid namespace format: $namespace"
return 1
fi
return 0
}
# Prerequisites checking with enhanced error handling
check_prerequisites() {
log_step "Checking Prerequisites"
local os=$(detect_os)
log_substep "Detected OS: $os"
local missing_tools=()
# Check required tools with installation guidance
for tool in kubectl helm openssl curl git; do
if ! command -v "$tool" &> /dev/null; then
missing_tools+=("$tool")
log_warning "$tool is not installed"
echo -e " ${BLUE}Install instructions:${NC} $(get_install_instructions "$tool" "$os")"
else
log_substep "β $tool installed"
fi
done
if [[ ${#missing_tools[@]} -ne 0 ]]; then
log_error "Missing required tools: ${missing_tools[*]}"
echo -e "\n${YELLOW}To install all tools on $os:${NC}"
case "$os" in
"macOS")
echo " brew install kubectl helm git"
echo " (openssl and curl are pre-installed)"
;;
"Linux")
echo " # Ubuntu/Debian:"
echo " sudo apt-get update && sudo apt-get install -y kubectl helm openssl curl git"
;;
"Windows")
echo " # Via Chocolatey:"
echo " choco install kubernetes-cli kubernetes-helm git"
;;
esac
echo
log_info "Please install the missing tools and run the script again"
return 1
fi
# Check Kubernetes connectivity with detailed guidance
if ! kubectl cluster-info &> /dev/null; then
log_error "Cannot connect to Kubernetes cluster"
echo
log_info "Troubleshooting steps:"
echo " 1. Ensure you have a Kubernetes cluster (DigitalOcean, AWS EKS, GKE, etc.)"
echo " 2. Configure kubectl with your cluster credentials"
echo " 3. Test connection: kubectl get nodes"
echo
echo " ${BLUE}For DigitalOcean Kubernetes:${NC}"
echo " doctl kubernetes cluster kubeconfig save <cluster-id>"
echo
return 1
fi
local cluster_info=$(kubectl cluster-info | head -1)
log_substep "β Connected to: ${cluster_info#*at }"
# Check if Helm chart exists
if [[ ! -d "$HELM_CHART_PATH" ]]; then
log_error "Helm chart not found at: $HELM_CHART_PATH"
log_info "Ensure you're running this script from the wordpress directory"
return 1
fi
log_substep "β Helm chart found"
# Check Helm dependencies
if [[ -f "$HELM_CHART_PATH/Chart.yaml" ]]; then
log_substep "β Helm Chart.yaml validated"
else
log_error "Invalid Helm chart: missing Chart.yaml"
return 1
fi
return 0
}
# Infrastructure setup
setup_infrastructure() {
log_step "Setting Up Infrastructure Prerequisites"
if kubectl get svc ingress-nginx-controller -n infra >/dev/null 2>&1 \
&& kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then
log_substep "Using existing ingress-nginx controller and ClusterIssuer 'letsencrypt-prod' (shared infra); skipping local installation"
return 0
fi
# Get cluster name for LoadBalancer naming (extract from current context)
local cluster_context=$(kubectl config current-context)
local cluster_name=$(echo "$cluster_context" | sed 's/.*@//' | sed 's/do-.*-k8s-//' | head -c 20)
# Check and install NGINX Ingress Controller
log_substep "Checking NGINX Ingress Controller..."
if ! kubectl get namespace ingress-nginx &> /dev/null || ! kubectl get svc -n ingress-nginx ingress-nginx-controller &> /dev/null; then
log_substep "Installing NGINX Ingress Controller..."
# Detect if this is a DigitalOcean cluster for LoadBalancer annotation
if kubectl get nodes -o jsonpath='{.items[0].spec.providerID}' | grep -q "digitalocean"; then
log_substep "Detected DigitalOcean cluster - configuring LoadBalancer..."
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer \
--set controller.service.annotations."service\.beta\.kubernetes\.io/do-loadbalancer-name"="${cluster_name}-nginx-ingress" \
--set controller.metrics.enabled=true \
--set controller.podAnnotations."prometheus\.io/scrape"="true" \
--set controller.podAnnotations."prometheus\.io/port"="10254" \
--set controller.config.use-proxy-protocol="false" \
--wait --timeout=300s
else
log_substep "Installing NGINX Ingress Controller (generic cloud)..."
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer \
--set controller.metrics.enabled=true \
--set controller.podAnnotations."prometheus\.io/scrape"="true" \
--set controller.podAnnotations."prometheus\.io/port"="10254" \
--wait --timeout=300s
fi
log_substep "Waiting for NGINX Ingress Controller to be ready..."
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=300s
else
log_substep "β NGINX Ingress Controller already installed"
fi
# CRITICAL: Ensure ingress-nginx namespace has the required label for NetworkPolicy
if ! kubectl get namespace ingress-nginx --show-labels | grep -q "name=ingress-nginx"; then
log_substep "Adding required NetworkPolicy label to ingress-nginx namespace..."
kubectl label namespace ingress-nginx name=ingress-nginx --overwrite
log_substep "β NetworkPolicy label added"
else
log_substep "β NetworkPolicy label already present"
fi
# Check and install cert-manager
log_substep "Checking cert-manager..."
if ! kubectl get namespace cert-manager &> /dev/null; then
log_substep "Installing cert-manager..."
helm repo add jetstack https://charts.jetstack.io &> /dev/null || true
helm repo update &> /dev/null
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.13.2 \
--set installCRDs=true \
--set securityContext.runAsNonRoot=true \
--set securityContext.runAsUser=1000 \
--wait --timeout=300s
log_substep "Waiting for cert-manager to be ready..."
kubectl wait --namespace cert-manager \
--for=condition=ready pod \
--selector=app.kubernetes.io/instance=cert-manager \
--timeout=300s
else
log_substep "β cert-manager already installed"
fi
# Create ClusterIssuer for Let's Encrypt if it doesn't exist
log_substep "Setting up Let's Encrypt ClusterIssuer..."
# Check if ClusterIssuer already exists
if kubectl get clusterissuer letsencrypt-prod &>/dev/null; then
# Update existing ClusterIssuer with proper email
log_substep "Updating ClusterIssuer with email: ${EMAIL}"
kubectl patch clusterissuer letsencrypt-prod --type='merge' -p="{\"spec\":{\"acme\":{\"email\":\"${EMAIL}\"}}}"
log_success "β
ClusterIssuer updated with email: ${EMAIL}"
else
# Create ClusterIssuer with proper Helm labels for ownership
cat <<EOF > /tmp/clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
labels:
app.kubernetes.io/managed-by: kubectl
annotations:
meta.helm.sh/managed-by: kubectl
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ${EMAIL}
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
EOF
kubectl apply -f /tmp/clusterissuer.yaml
rm -f /tmp/clusterissuer.yaml
log_success "β
ClusterIssuer configured with email: ${EMAIL}"
fi
# Using official WordPress and MariaDB images - no external dependencies needed
}
# WordPress uses installation wizard - no password needed
generate_secure_password() {
openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
}
generate_wp_salt() {
openssl rand -hex 64
}
# Enhanced password hashing with Argon2id (matching Vaultwarden security)
generate_argon2_hash() {
local password="$1"
local os=$(detect_os)
# Check for argon2 binary in common locations
local argon2_bin=""
for path in "/opt/homebrew/bin/argon2" "$(which argon2 2>/dev/null)" "/usr/bin/argon2" "/usr/local/bin/argon2"; do
if [[ -x "$path" ]]; then
argon2_bin="$path"
break
fi
done
if [[ -n "$argon2_bin" ]]; then
# Use enterprise-grade Argon2id parameters (64MB memory, 3 iterations, 4 threads)
log_substep "Using argon2 binary: $argon2_bin"
echo -n "$password" | "$argon2_bin" "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
return 0
else
log_warning "argon2 CLI not found - attempting installation..."
case "$os" in
"macOS")
if command -v brew &> /dev/null; then
log_info "Installing argon2 via Homebrew..."
if brew install argon2 &> /dev/null; then
for path in "/opt/homebrew/bin/argon2" "/usr/local/bin/argon2"; do
if [[ -x "$path" ]]; then
echo -n "$password" | "$path" "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
return 0
fi
done
fi
fi
;;
"Linux")
if command -v apt-get &> /dev/null; then
log_info "Installing argon2 via apt-get..."
if sudo apt-get update -qq && sudo apt-get install -y argon2 &> /dev/null; then
echo -n "$password" | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
return 0
fi
fi
;;
esac
# Fallback to bcrypt-style hashing if argon2 unavailable
log_warning "argon2 not available, using OpenSSL-based secure hash"
openssl passwd -6 "$password"
fi
}
# Enterprise domain and DNS configuration
collect_user_input() {
log_step "WordPress Enterprise Configuration"
echo
log_info "π’ Welcome to Enterprise WordPress Deployment"
log_info "This deployment creates a production-ready WordPress site with:"
echo " β’ π Enterprise-grade security (Pod Security Standards: Restricted)"
echo " β’ π‘οΈ Zero-trust networking with NetworkPolicy"
echo " β’ π Resource optimization for cluster efficiency"
echo " β’ β‘ TLS 1.3 with automated Let's Encrypt certificates"
echo " β’ π Secure credential management via Kubernetes secrets"
echo
# Step 0: Namespace and Release Name Configuration
if [[ -z "$NAMESPACE" ]] || [[ -z "$RELEASE_NAME" ]]; then
prompt_namespace_and_release
fi
# Step 1: Domain Configuration
if [[ -n "$DOMAIN" ]]; then
log_success "Using configured domain: $DOMAIN"
FULL_DOMAIN="$DOMAIN"
# Check if it's a subdomain or main domain
if [[ "$DOMAIN" =~ ^[a-zA-Z0-9-]+\.[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
# Has subdomain prefix (e.g., blog.example.com)
SUBDOMAIN=$(echo "$DOMAIN" | cut -d'.' -f1)
MAIN_DOMAIN=$(echo "$DOMAIN" | cut -d'.' -f2-)
INCLUDE_WWW=false # Don't add www to subdomains
else
# Main domain (e.g., example.com)
MAIN_DOMAIN="$DOMAIN"
INCLUDE_WWW=true # Default to including www for main domains in CLI mode
fi
else
log_step "β’ Domain Configuration"
echo
while true; do
echo "Choose your WordPress deployment type:"
echo " 1) Main domain (e.g., yourdomain.com)"
echo " 2) Subdomain (e.g., blog.yourdomain.com)"
echo
echo -n -e "${WHITE}Select option [1-2]: ${NC}"
read -r domain_choice
case $domain_choice in
1)
log_info "Selected: Main domain deployment"
echo
while true; do
echo -n -e "${WHITE}Enter your domain (e.g., yourdomain.com): ${NC}"
read -r DOMAIN
if validate_domain "$DOMAIN"; then
MAIN_DOMAIN="$DOMAIN"
FULL_DOMAIN="$DOMAIN"
break
fi
log_warning "Invalid domain format. Please enter a valid domain (e.g., example.com)"
done
# Ask about www subdomain support
echo
echo -e "${BOLD}WWW Subdomain Configuration:${NC}"
while true; do
echo -n -e "${WHITE}Include www.${DOMAIN} with automatic configuration? [Y/n]: ${NC}"
read -r www_choice
case "${www_choice:-Y}" in
[Yy]|[Yy][Ee][Ss]|"")
INCLUDE_WWW=true
log_success "Will configure both ${DOMAIN} and www.${DOMAIN}"
# Ask about redirect preference
echo
echo -e "${BOLD}Domain Redirect Configuration:${NC}"
echo " Choose your canonical (primary) domain:"
echo
echo " ${CYAN}Option 1:${NC} Redirect TO www (${DOMAIN} β www.${DOMAIN})"
echo " β’ Traditional enterprise best practice"
echo " β’ Used by: Microsoft, Facebook, Wikipedia"
echo " β’ Better for CDN flexibility"
echo
echo " ${CYAN}Option 2:${NC} Redirect FROM www (www.${DOMAIN} β ${DOMAIN})"
echo " β’ Modern SaaS best practice"
echo " β’ Used by: Google, Apple, Amazon"
echo " β’ Shorter, cleaner URLs"
echo
echo " ${CYAN}Option 3:${NC} No redirect (both URLs work independently)"
echo " β’ Not recommended: creates SEO duplicate content issues"
echo
while true; do
echo -n -e "${WHITE}Select redirect preference [1/2/3]: ${NC}"
read -r redirect_choice
case "$redirect_choice" in
1)
REDIRECT_TO_WWW=true
REDIRECT_FROM_WWW=false
log_success "β Will redirect ${DOMAIN} β www.${DOMAIN}"
break
;;
2)
REDIRECT_TO_WWW=false
REDIRECT_FROM_WWW=true
log_success "β Will redirect www.${DOMAIN} β ${DOMAIN}"
break
;;
3)
REDIRECT_TO_WWW=false
REDIRECT_FROM_WWW=false
log_warning "β οΈ Both URLs will work (not recommended for SEO)"
break
;;
*)
log_warning "Please enter 1, 2, or 3"
;;
esac
done
break
;;
[Nn]|[Nn][Oo])
INCLUDE_WWW=false
REDIRECT_TO_WWW=false
REDIRECT_FROM_WWW=false
log_info "Will configure ${DOMAIN} only"
break
;;
*)
log_warning "Please enter Y or N"
;;
esac
done
break
;;
2)
log_info "Selected: Subdomain deployment"
echo
while true; do
echo -n -e "${WHITE}Enter your main domain (e.g., yourdomain.com): ${NC}"
read -r MAIN_DOMAIN
if validate_domain "$MAIN_DOMAIN"; then
break
fi
log_warning "Invalid domain format. Please enter a valid domain (e.g., example.com)"
done
while true; do
echo -n -e "${WHITE}Enter subdomain prefix (e.g., blog, www, app): ${NC}"
read -r SUBDOMAIN
if [[ "$SUBDOMAIN" =~ ^[a-zA-Z0-9-]+$ ]] && [[ ${#SUBDOMAIN} -le 63 ]]; then
DOMAIN="${SUBDOMAIN}.${MAIN_DOMAIN}"
FULL_DOMAIN="$DOMAIN"
INCLUDE_WWW=false # Initialize for subdomain deployment
break
fi
log_warning "Invalid subdomain. Use only letters, numbers, and hyphens"
done
break
;;
*)
log_warning "Please enter 1 or 2"
;;
esac
done
fi
# Step 2: Email Configuration
if [[ -n "$EMAIL" ]]; then
log_success "Using configured email: $EMAIL"
else
echo
while true; do
echo -n -e "${WHITE}Enter email for Let's Encrypt certificates: ${NC}"
read -r EMAIL
if validate_email "$EMAIL"; then
break
fi
log_warning "Invalid email format. Please enter a valid email address."
done
fi
# Step 3: Namespace and Release Configuration
prompt_namespace_and_release
}
# DNS Configuration and Validation
validate_dns_configuration() {
log_info "Validating DNS configuration..."
if [[ -z "$DOMAIN" ]]; then
log_error "Domain not configured"
return 1
fi
local external_ip
external_ip=$(get_external_ip)
if [[ -z "$external_ip" ]]; then
log_warning "Cannot detect external IP for DNS validation"
return 1
fi
log_success "Domain: $DOMAIN"
log_success "External IP: $external_ip"
log_info "Ensure A record points $DOMAIN β $external_ip"
return 0
}
setup_dns_instructions() {
echo
log_step "π DNS Configuration Required"
echo
log_info "Before deploying WordPress, you need to configure DNS:"
echo