tayo 0.1.13 β†’ 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "json"
5
+
6
+ module Tayo
7
+ module Proxy
8
+ class DockerManager
9
+ def check_containers
10
+ puts "\n🐳 Docker μ»¨ν…Œμ΄λ„ˆ μƒνƒœλ₯Ό ν™•μΈν•©λ‹ˆλ‹€...".colorize(:yellow)
11
+
12
+ unless docker_installed?
13
+ puts "❌ Dockerκ°€ μ„€μΉ˜λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.".colorize(:red)
14
+ puts "https://www.docker.com/get-started μ—μ„œ Dockerλ₯Ό μ„€μΉ˜ν•΄μ£Όμ„Έμš”.".colorize(:cyan)
15
+ exit 1
16
+ end
17
+
18
+ unless docker_running?
19
+ puts "❌ Dockerκ°€ μ‹€ν–‰λ˜κ³  μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.".colorize(:red)
20
+ puts "Docker Desktop을 μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:cyan)
21
+ exit 1
22
+ end
23
+
24
+ # Kamal Proxy μƒνƒœ 확인
25
+ check_kamal_proxy_status
26
+
27
+ # Caddy μƒνƒœ 확인
28
+ check_caddy_status
29
+
30
+ puts ""
31
+ end
32
+
33
+ def container_exists?(name)
34
+ output = `docker ps -a --filter "name=^#{name}$" --format "{{.Names}}" 2>/dev/null`.strip
35
+ !output.empty?
36
+ end
37
+
38
+ def container_running?(name)
39
+ output = `docker ps --filter "name=^#{name}$" --format "{{.Names}}" 2>/dev/null`.strip
40
+ !output.empty?
41
+ end
42
+
43
+ def check_port_binding(container, ports)
44
+ return false unless container_running?(container)
45
+
46
+ ports.all? do |port|
47
+ output = `docker port #{container} #{port} 2>/dev/null`.strip
48
+ !output.empty?
49
+ end
50
+ end
51
+
52
+ def port_in_use?(port)
53
+ # Docker μ»¨ν…Œμ΄λ„ˆκ°€ 포트λ₯Ό μ‚¬μš© 쀑인지 확인
54
+ docker_using = `docker ps --format "table {{.Names}}\t{{.Ports}}" | grep -E "0\\.0\\.0\\.0:#{port}->|\\*:#{port}->" 2>/dev/null`.strip
55
+ return true unless docker_using.empty?
56
+
57
+ # μ‹œμŠ€ν…œ 포트 확인
58
+ if RUBY_PLATFORM.include?("darwin")
59
+ # macOS
60
+ output = `lsof -iTCP:#{port} -sTCP:LISTEN 2>/dev/null`.strip
61
+ else
62
+ # Linux
63
+ output = `netstat -tln 2>/dev/null | grep ":#{port}"`.strip
64
+ output = `ss -tln 2>/dev/null | grep ":#{port}"`.strip if output.empty?
65
+ end
66
+
67
+ !output.empty?
68
+ end
69
+
70
+ def stop_container(name)
71
+ return unless container_exists?(name)
72
+
73
+ puts "πŸ›‘ #{name} μ»¨ν…Œμ΄λ„ˆλ₯Ό μ€‘μ§€ν•©λ‹ˆλ‹€...".colorize(:yellow)
74
+ system("docker stop #{name} >/dev/null 2>&1")
75
+ system("docker rm #{name} >/dev/null 2>&1")
76
+ end
77
+
78
+ def get_container_network(name)
79
+ return nil unless container_running?(name)
80
+
81
+ output = `docker inspect #{name} --format '{{range .NetworkSettings.Networks}}{{.NetworkID}}{{end}}' 2>/dev/null`.strip
82
+ output.empty? ? nil : output
83
+ end
84
+
85
+ def create_network_if_not_exists(network_name = "tayo-proxy")
86
+ existing = `docker network ls --filter "name=^#{network_name}$" --format "{{.Name}}" 2>/dev/null`.strip
87
+
88
+ if existing.empty?
89
+ puts "πŸ“‘ Docker λ„€νŠΈμ›Œν¬ '#{network_name}'λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€...".colorize(:yellow)
90
+ system("docker network create #{network_name} >/dev/null 2>&1")
91
+ end
92
+
93
+ network_name
94
+ end
95
+
96
+ private
97
+
98
+ def docker_installed?
99
+ system("which docker >/dev/null 2>&1")
100
+ end
101
+
102
+ def docker_running?
103
+ system("docker info >/dev/null 2>&1")
104
+ end
105
+
106
+ def check_kamal_proxy_status
107
+ # Traefik으둜 κ΅μ²΄λ˜μ–΄ 더 이상 μ‚¬μš©ν•˜μ§€ μ•ŠμŒ
108
+ check_traefik_status
109
+ end
110
+
111
+ def check_traefik_status
112
+ if container_running?("traefik")
113
+ if check_port_binding("traefik", [80, 443])
114
+ puts "βœ… Traefik: μ‹€ν–‰ 쀑 (80, 443 포트 μ‚¬μš©)".colorize(:green)
115
+ else
116
+ puts "⚠️ Traefik: μ‹€ν–‰ μ€‘μ΄μ§€λ§Œ ν¬νŠΈκ°€ μ˜¬λ°”λ₯΄κ²Œ λ°”μΈλ”©λ˜μ§€ μ•ŠμŒ".colorize(:yellow)
117
+ show_port_conflicts
118
+ end
119
+ elsif container_exists?("traefik")
120
+ puts "⚠️ Traefik: 쀑지됨".colorize(:yellow)
121
+ else
122
+ puts "ℹ️ Traefik: μ„€μΉ˜λ˜μ§€ μ•ŠμŒ".colorize(:gray)
123
+ end
124
+ end
125
+
126
+ def check_caddy_status
127
+ # CaddyλŠ” 더 이상 μ‚¬μš©ν•˜μ§€ μ•ŠμŒ
128
+ # 이 λ©”μ„œλ“œλŠ” ν•˜μœ„ ν˜Έν™˜μ„±μ„ μœ„ν•΄ μœ μ§€ν•˜μ§€λ§Œ 아무것도 ν•˜μ§€ μ•ŠμŒ
129
+ end
130
+
131
+ def show_port_conflicts
132
+ [80, 443].each do |port|
133
+ if port_in_use?(port)
134
+ # 포트λ₯Ό μ‚¬μš© 쀑인 ν”„λ‘œμ„ΈμŠ€ μ°ΎκΈ°
135
+ if RUBY_PLATFORM.include?("darwin")
136
+ process = `lsof -iTCP:#{port} -sTCP:LISTEN 2>/dev/null | grep LISTEN | head -1`.strip
137
+ else
138
+ process = `netstat -tlnp 2>/dev/null | grep ":#{port}" | head -1`.strip
139
+ end
140
+
141
+ unless process.empty?
142
+ puts " ⚠️ 포트 #{port}κ°€ λ‹€λ₯Έ ν”„λ‘œμ„ΈμŠ€μ—μ„œ μ‚¬μš© μ€‘μž…λ‹ˆλ‹€:".colorize(:yellow)
143
+ puts " #{process}".colorize(:gray)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "tty-prompt"
5
+
6
+ module Tayo
7
+ module Proxy
8
+ class NetworkConfig
9
+ attr_reader :public_ip, :internal_ip, :external_http, :external_https
10
+
11
+ def initialize
12
+ @prompt = TTY::Prompt.new
13
+ @use_custom_ports = false
14
+ end
15
+
16
+ def detect_ips
17
+ puts "\nπŸ” λ„€νŠΈμ›Œν¬ 정보λ₯Ό ν™•μΈν•©λ‹ˆλ‹€...".colorize(:yellow)
18
+
19
+ # 곡인 IP 감지
20
+ print "곡인 IP 확인 쀑... "
21
+ @public_ip = detect_public_ip
22
+ puts "#{@public_ip}".colorize(:green)
23
+
24
+ # λ‚΄λΆ€ IP 감지
25
+ print "λ‚΄λΆ€ IP 확인 쀑... "
26
+ @internal_ip = detect_internal_ip
27
+ puts "#{@internal_ip}".colorize(:green)
28
+
29
+ puts ""
30
+ end
31
+
32
+ def configure_ports
33
+ puts "\n🌐 μ™ΈλΆ€ 접속 포트λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.".colorize(:yellow)
34
+ puts "Kamal ProxyλŠ” 항상 80, 443 포트λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.".colorize(:gray)
35
+ puts ""
36
+
37
+ choices = [
38
+ { name: "κ³΅μœ κΈ°μ—μ„œ 80, 443을 직접 ν¬μ›Œλ”© (κΈ°λ³Έ)", value: :direct },
39
+ { name: "λ‹€λ₯Έ 포트λ₯Ό μ‚¬μš©ν•˜μ—¬ ν¬μ›Œλ”© (예: 8080β†’80, 8443β†’443)", value: :custom }
40
+ ]
41
+
42
+ choice = @prompt.select("포트 μ„€μ • 방식을 μ„ νƒν•˜μ„Έμš”:", choices)
43
+
44
+ if choice == :custom
45
+ @use_custom_ports = true
46
+
47
+ @external_http = @prompt.ask("HTTP μ™ΈλΆ€ 포트 (κΈ°λ³Έ: 8080):", default: "8080")
48
+ @external_https = @prompt.ask("HTTPS μ™ΈλΆ€ 포트 (κΈ°λ³Έ: 8443):", default: "8443")
49
+
50
+ puts "\nβœ… μ™ΈλΆ€ ν¬νŠΈκ°€ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€:".colorize(:green)
51
+ puts " HTTP: #{@external_http}".colorize(:gray)
52
+ puts " HTTPS: #{@external_https}".colorize(:gray)
53
+ else
54
+ @external_http = "80"
55
+ @external_https = "443"
56
+
57
+ puts "\nβœ… ν‘œμ€€ 포트(80, 443)λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.".colorize(:green)
58
+ end
59
+ end
60
+
61
+ def use_custom_ports?
62
+ @use_custom_ports
63
+ end
64
+
65
+ def show_port_forwarding_guide
66
+ return unless use_custom_ports?
67
+
68
+ puts "\nπŸ“‘ 곡유기 ν¬νŠΈν¬μ›Œλ”© μ„€μ • μ•ˆλ‚΄:".colorize(:yellow)
69
+ puts "━" * 50
70
+ puts "μ™ΈλΆ€ 포트 #{@external_http} β†’ #{@internal_ip}:80".colorize(:white)
71
+ puts "μ™ΈλΆ€ 포트 #{@external_https} β†’ #{@internal_ip}:443".colorize(:white)
72
+ puts "━" * 50
73
+ puts ""
74
+ puts "μœ„ 섀정을 곡유기 관리 νŽ˜μ΄μ§€μ—μ„œ μ™„λ£Œν•΄μ£Όμ„Έμš”.".colorize(:cyan)
75
+ puts "일반적인 접속 μ£Όμ†Œ: http://192.168.1.1".colorize(:gray)
76
+ end
77
+
78
+ private
79
+
80
+ def detect_public_ip
81
+ # curl을 μ‚¬μš©ν•œ 곡인 IP 감지
82
+ ip = `curl -s ifconfig.me 2>/dev/null`.strip
83
+
84
+ # λŒ€μ²΄ 방법듀
85
+ if ip.empty? || !valid_ip?(ip)
86
+ ip = `curl -s ipecho.net/plain 2>/dev/null`.strip
87
+ end
88
+
89
+ if ip.empty? || !valid_ip?(ip)
90
+ ip = `curl -s icanhazip.com 2>/dev/null`.strip
91
+ end
92
+
93
+ if ip.empty? || !valid_ip?(ip)
94
+ puts "\n⚠️ 곡인 IPλ₯Ό μžλ™μœΌλ‘œ 감지할 수 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
95
+ ip = @prompt.ask("곡인 IPλ₯Ό 직접 μž…λ ₯ν•΄μ£Όμ„Έμš”:") do |q|
96
+ q.validate(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/, "μ˜¬λ°”λ₯Έ IP ν˜•μ‹μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”")
97
+ end
98
+ end
99
+
100
+ ip
101
+ end
102
+
103
+ def detect_internal_ip
104
+ # macOS
105
+ if RUBY_PLATFORM.include?("darwin")
106
+ # en0 (Wi-Fi) λ˜λŠ” en1 (Ethernet) μΈν„°νŽ˜μ΄μŠ€μ—μ„œ IP μΆ”μΆœ
107
+ ip = `ifconfig en0 2>/dev/null | grep 'inet ' | grep -v 127.0.0.1 | awk '{print $2}'`.strip
108
+ ip = `ifconfig en1 2>/dev/null | grep 'inet ' | grep -v 127.0.0.1 | awk '{print $2}'`.strip if ip.empty?
109
+
110
+ # λ‹€λ₯Έ μΈν„°νŽ˜μ΄μŠ€ 검색
111
+ if ip.empty?
112
+ ip = `ifconfig | grep 'inet ' | grep -v 127.0.0.1 | grep -v '::1' | head -1 | awk '{print $2}'`.strip
113
+ end
114
+ else
115
+ # Linux
116
+ ip = `hostname -I 2>/dev/null | awk '{print $1}'`.strip
117
+
118
+ if ip.empty?
119
+ ip = `ip addr show | grep 'inet ' | grep -v 127.0.0.1 | grep -v '::1' | head -1 | awk '{print $2}' | cut -d/ -f1`.strip
120
+ end
121
+ end
122
+
123
+ # μ—¬μ „νžˆ λΉ„μ–΄μžˆλ‹€λ©΄ μˆ˜λ™ μž…λ ₯
124
+ if ip.empty? || !valid_ip?(ip)
125
+ puts "\n⚠️ λ‚΄λΆ€ IPλ₯Ό μžλ™μœΌλ‘œ 감지할 수 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
126
+ ip = @prompt.ask("λ‚΄λΆ€ IPλ₯Ό 직접 μž…λ ₯ν•΄μ£Όμ„Έμš” (예: 192.168.1.100):") do |q|
127
+ q.validate(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/, "μ˜¬λ°”λ₯Έ IP ν˜•μ‹μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”")
128
+ end
129
+ end
130
+
131
+ ip
132
+ end
133
+
134
+ def valid_ip?(ip)
135
+ return false if ip.nil? || ip.empty?
136
+
137
+ parts = ip.split('.')
138
+ return false unless parts.length == 4
139
+
140
+ parts.all? do |part|
141
+ num = part.to_i
142
+ num >= 0 && num <= 255 && part == num.to_s
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "fileutils"
5
+ require "yaml"
6
+ require "erb"
7
+
8
+ module Tayo
9
+ module Proxy
10
+ class TraefikConfig
11
+ TRAEFIK_CONFIG_DIR = File.expand_path("~/.tayo/traefik")
12
+
13
+ def initialize
14
+ @docker = DockerManager.new
15
+ end
16
+
17
+ def setup(domains, email = nil)
18
+ puts "\nβš™οΈ Traefik을 μ„€μ •ν•©λ‹ˆλ‹€...".colorize(:yellow)
19
+
20
+ # μ„€μ • 디렉토리 생성
21
+ setup_directories
22
+
23
+ # 이메일 μ£Όμ†Œ μž…λ ₯ (Let's Encrypt용)
24
+ email ||= get_email_for_acme
25
+
26
+ # μ„€μ • 파일 생성
27
+ create_docker_compose(email)
28
+ create_traefik_config(email)
29
+ create_dynamic_config(domains)
30
+
31
+ # Traefik μ‹œμž‘
32
+ ensure_running
33
+
34
+ # λΌμš°νŒ… μ„€μ •
35
+ configure_routes(domains)
36
+
37
+ puts "βœ… Traefik 섀정이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
38
+ end
39
+
40
+ private
41
+
42
+ def setup_directories
43
+ FileUtils.mkdir_p(TRAEFIK_CONFIG_DIR)
44
+ FileUtils.mkdir_p(File.join(TRAEFIK_CONFIG_DIR, "config"))
45
+
46
+ # acme.json 파일 생성 및 κΆŒν•œ μ„€μ •
47
+ acme_file = File.join(TRAEFIK_CONFIG_DIR, "acme.json")
48
+ unless File.exist?(acme_file)
49
+ File.write(acme_file, "{}")
50
+ File.chmod(0600, acme_file)
51
+ end
52
+ end
53
+
54
+ def get_email_for_acme
55
+ prompt = TTY::Prompt.new
56
+
57
+ # μ €μž₯된 이메일 확인
58
+ email_file = File.join(TRAEFIK_CONFIG_DIR, ".email")
59
+ if File.exist?(email_file)
60
+ saved_email = File.read(email_file).strip
61
+ if prompt.yes?("μ €μž₯된 이메일을 μ‚¬μš©ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ? (#{saved_email})")
62
+ return saved_email
63
+ end
64
+ end
65
+
66
+ # μƒˆ 이메일 μž…λ ₯
67
+ email = prompt.ask("Let's Encrypt μΈμ¦μ„œμš© 이메일 μ£Όμ†Œλ₯Ό μž…λ ₯ν•˜μ„Έμš”:") do |q|
68
+ q.validate(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, "μ˜¬λ°”λ₯Έ 이메일 ν˜•μ‹μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”")
69
+ end
70
+
71
+ # 이메일 μ €μž₯
72
+ File.write(email_file, email)
73
+ email
74
+ end
75
+
76
+ def create_docker_compose(email)
77
+ compose_content = <<~YAML
78
+ version: '3.8'
79
+
80
+ services:
81
+ traefik:
82
+ image: traefik:v3.0
83
+ container_name: traefik
84
+ restart: unless-stopped
85
+ security_opt:
86
+ - no-new-privileges:true
87
+ networks:
88
+ - traefik-net
89
+ ports:
90
+ - "80:80"
91
+ - "443:443"
92
+ - "8080:8080" # λŒ€μ‹œλ³΄λ“œ
93
+ extra_hosts:
94
+ - "host.docker.internal:host-gateway"
95
+ volumes:
96
+ - /var/run/docker.sock:/var/run/docker.sock:ro
97
+ - #{TRAEFIK_CONFIG_DIR}/config/traefik.yml:/etc/traefik/traefik.yml:ro
98
+ - #{TRAEFIK_CONFIG_DIR}/config/dynamic.yml:/etc/traefik/dynamic.yml:ro
99
+ - #{TRAEFIK_CONFIG_DIR}/acme.json:/acme.json
100
+ labels:
101
+ - "traefik.enable=true"
102
+ - "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)"
103
+ - "traefik.http.routers.dashboard.service=api@internal"
104
+ - "traefik.http.routers.dashboard.middlewares=auth"
105
+ - "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$10$$YFPx3EmK6lN5bPG.zPNvp.UYQhkPvNnkZ7J4zYu2GODXJfHZXfYbK" # admin:admin
106
+
107
+ networks:
108
+ traefik-net:
109
+ name: traefik-net
110
+ driver: bridge
111
+ YAML
112
+
113
+ compose_file = File.join(TRAEFIK_CONFIG_DIR, "docker-compose.yml")
114
+ File.write(compose_file, compose_content)
115
+ puts "βœ… Docker Compose 파일이 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
116
+ end
117
+
118
+ def create_traefik_config(email = nil)
119
+ # 이메일 μ£Όμ†Œ 확인
120
+ email_file = File.join(TRAEFIK_CONFIG_DIR, ".email")
121
+ email ||= File.exist?(email_file) ? File.read(email_file).strip : "admin@example.com"
122
+
123
+ config_content = <<~YAML
124
+ # Traefik 정적 μ„€μ •
125
+ api:
126
+ dashboard: true
127
+ debug: false
128
+
129
+ entryPoints:
130
+ web:
131
+ address: ":80"
132
+ http:
133
+ redirections:
134
+ entryPoint:
135
+ to: websecure
136
+ scheme: https
137
+ permanent: true
138
+ websecure:
139
+ address: ":443"
140
+
141
+ providers:
142
+ docker:
143
+ endpoint: "unix:///var/run/docker.sock"
144
+ exposedByDefault: false
145
+ network: traefik-net
146
+ watch: true
147
+ file:
148
+ filename: /etc/traefik/dynamic.yml
149
+ watch: true
150
+
151
+ certificatesResolvers:
152
+ myresolver:
153
+ acme:
154
+ email: #{email}
155
+ storage: /acme.json
156
+ tlsChallenge: {}
157
+ # caServer: https://acme-staging-v02.api.letsencrypt.org/directory # ν…ŒμŠ€νŠΈμš©
158
+
159
+ log:
160
+ level: INFO
161
+ format: json
162
+
163
+ accessLog:
164
+ format: json
165
+ YAML
166
+
167
+ config_file = File.join(TRAEFIK_CONFIG_DIR, "config", "traefik.yml")
168
+ File.write(config_file, config_content)
169
+ puts "βœ… Traefik μ„€μ • 파일이 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
170
+ end
171
+
172
+ def create_dynamic_config(domains)
173
+ routers = {}
174
+ services = {}
175
+
176
+ domains.each do |domain_info|
177
+ domain = domain_info[:domain]
178
+ safe_name = domain.gsub('.', '-').gsub('_', '-')
179
+
180
+ # HTTP λΌμš°ν„° (λ¦¬λ‹€μ΄λ ‰νŠΈμš©)
181
+ routers["#{safe_name}-http"] = {
182
+ "rule" => "Host(`#{domain}`)",
183
+ "entryPoints" => ["web"],
184
+ "middlewares" => ["redirect-to-https"],
185
+ "service" => "#{safe_name}-service"
186
+ }
187
+
188
+ # HTTPS λΌμš°ν„°
189
+ routers["#{safe_name}-https"] = {
190
+ "rule" => "Host(`#{domain}`)",
191
+ "entryPoints" => ["websecure"],
192
+ "service" => "#{safe_name}-service",
193
+ "tls" => {
194
+ "certResolver" => "myresolver"
195
+ }
196
+ }
197
+
198
+ # μ„œλΉ„μŠ€ μ •μ˜ (호슀트의 3000 포트둜)
199
+ services["#{safe_name}-service"] = {
200
+ "loadBalancer" => {
201
+ "servers" => [
202
+ { "url" => "http://host.docker.internal:3000" }
203
+ ]
204
+ }
205
+ }
206
+ end
207
+
208
+ # 미듀웨어 μ •μ˜
209
+ dynamic_config = {
210
+ "http" => {
211
+ "middlewares" => {
212
+ "redirect-to-https" => {
213
+ "redirectScheme" => {
214
+ "scheme" => "https",
215
+ "permanent" => true
216
+ }
217
+ }
218
+ },
219
+ "routers" => routers,
220
+ "services" => services
221
+ }
222
+ }
223
+
224
+ dynamic_file = File.join(TRAEFIK_CONFIG_DIR, "config", "dynamic.yml")
225
+ File.write(dynamic_file, dynamic_config.to_yaml)
226
+ puts "βœ… 동적 λΌμš°νŒ… 섀정이 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
227
+ end
228
+
229
+ def ensure_running
230
+ if @docker.container_running?("traefik")
231
+ puts "πŸ”„ Traefik μ»¨ν…Œμ΄λ„ˆλ₯Ό μž¬μ‹œμž‘ν•©λ‹ˆλ‹€...".colorize(:yellow)
232
+ reload_traefik
233
+ else
234
+ start_container
235
+ end
236
+ end
237
+
238
+ def start_container
239
+ puts "πŸš€ Traefik μ»¨ν…Œμ΄λ„ˆλ₯Ό μ‹œμž‘ν•©λ‹ˆλ‹€...".colorize(:yellow)
240
+
241
+ # κΈ°μ‘΄ μ»¨ν…Œμ΄λ„ˆκ°€ μžˆλ‹€λ©΄ 제거
242
+ if @docker.container_exists?("traefik")
243
+ @docker.stop_container("traefik")
244
+ end
245
+
246
+ # Docker Compose둜 μ‹œμž‘
247
+ Dir.chdir(TRAEFIK_CONFIG_DIR) do
248
+ if system("docker compose up -d")
249
+ puts "βœ… Traefik이 μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
250
+
251
+ # μ‹œμž‘ μ™„λ£Œ λŒ€κΈ°
252
+ sleep 3
253
+
254
+ # μƒνƒœ 확인
255
+ check_traefik_status
256
+ else
257
+ puts "❌ Traefik μ‹œμž‘μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
258
+ exit 1
259
+ end
260
+ end
261
+ end
262
+
263
+ def reload_traefik
264
+ puts "πŸ”„ Traefik 섀정을 λ‹€μ‹œ λ‘œλ“œν•©λ‹ˆλ‹€...".colorize(:yellow)
265
+
266
+ Dir.chdir(TRAEFIK_CONFIG_DIR) do
267
+ if system("docker compose restart")
268
+ puts "βœ… Traefik이 μž¬μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
269
+ else
270
+ puts "⚠️ Traefik μž¬μ‹œμž‘μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
271
+ end
272
+ end
273
+ end
274
+
275
+ def check_traefik_status
276
+ # λŒ€μ‹œλ³΄λ“œ μ ‘κ·Ό 확인
277
+ puts "\nπŸ“Š Traefik λŒ€μ‹œλ³΄λ“œ: http://localhost:8080".colorize(:cyan)
278
+ puts " (기본 인증: admin / admin)".colorize(:gray)
279
+
280
+ # 둜그 확인
281
+ logs = `docker logs traefik --tail 5 2>&1`.strip
282
+ if logs.include?("error") || logs.include?("Error")
283
+ puts "\n⚠️ Traefik λ‘œκ·Έμ— 였λ₯˜κ°€ λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€:".colorize(:yellow)
284
+ puts logs.colorize(:gray)
285
+ end
286
+ end
287
+
288
+ def configure_routes(domains)
289
+ puts "\nπŸ“ 도메인 λΌμš°νŒ… μƒνƒœ:".colorize(:yellow)
290
+
291
+ domains.each do |domain_info|
292
+ domain = domain_info[:domain]
293
+ puts " β€’ #{domain} β†’ localhost:3000".colorize(:green)
294
+ puts " HTTP: http://#{domain} (β†’ HTTPS λ¦¬λ‹€μ΄λ ‰νŠΈ)".colorize(:gray)
295
+ puts " HTTPS: https://#{domain} (Let's Encrypt μΈμ¦μ„œ)".colorize(:gray)
296
+ end
297
+
298
+ puts "\nπŸ’‘ Let's Encrypt μΈμ¦μ„œ λ°œκΈ‰ 쀑...".colorize(:yellow)
299
+ puts " 첫 λ°œκΈ‰μ—λŠ” 1-2뢄이 μ†Œμš”λ  수 μžˆμŠ΅λ‹ˆλ‹€.".colorize(:gray)
300
+ end
301
+ end
302
+ end
303
+ end