tayo 0.1.12 โ†’ 0.2.0

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,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