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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -57
- data/README.md +83 -45
- data/lib/tayo/cli.rb +6 -0
- data/lib/tayo/commands/cf.rb +116 -162
- data/lib/tayo/commands/init.rb +35 -35
- data/lib/tayo/commands/proxy.rb +97 -0
- data/lib/tayo/proxy/cloudflare_client.rb +323 -0
- data/lib/tayo/proxy/docker_manager.rb +150 -0
- data/lib/tayo/proxy/network_config.rb +147 -0
- data/lib/tayo/proxy/traefik_config.rb +303 -0
- data/lib/tayo/proxy/welcome_service.rb +337 -0
- data/lib/tayo/version.rb +1 -1
- data/lib/templates/welcome/Dockerfile +14 -0
- data/lib/templates/welcome/index.html +173 -0
- metadata +24 -3
- data/CLAUDE.md +0 -58
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
require_relative "../proxy/cloudflare_client"
|
|
6
|
+
require_relative "../proxy/docker_manager"
|
|
7
|
+
require_relative "../proxy/network_config"
|
|
8
|
+
require_relative "../proxy/traefik_config"
|
|
9
|
+
require_relative "../proxy/welcome_service"
|
|
10
|
+
|
|
11
|
+
module Tayo
|
|
12
|
+
module Commands
|
|
13
|
+
class Proxy
|
|
14
|
+
def execute
|
|
15
|
+
puts "π Kamal Proxyμ Caddy μ€μ μ μμν©λλ€...".colorize(:green)
|
|
16
|
+
puts ""
|
|
17
|
+
|
|
18
|
+
# 1. Cloudflare μ€μ
|
|
19
|
+
cloudflare = Tayo::Proxy::CloudflareClient.new
|
|
20
|
+
cloudflare.ensure_token
|
|
21
|
+
|
|
22
|
+
# 2. λ€νΈμν¬ μ€μ
|
|
23
|
+
network = Tayo::Proxy::NetworkConfig.new
|
|
24
|
+
network.detect_ips
|
|
25
|
+
network.configure_ports
|
|
26
|
+
|
|
27
|
+
# 3. Docker νμΈ
|
|
28
|
+
docker = Tayo::Proxy::DockerManager.new
|
|
29
|
+
docker.check_containers
|
|
30
|
+
|
|
31
|
+
# 4. λλ©μΈ μ ν
|
|
32
|
+
selected_domains = cloudflare.select_domains
|
|
33
|
+
|
|
34
|
+
if selected_domains.empty?
|
|
35
|
+
puts "β λλ©μΈμ΄ μ νλμ§ μμμ΅λλ€. μ’
λ£ν©λλ€.".colorize(:red)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 5. DNS μ€μ
|
|
40
|
+
puts "\nπ DNS λ μ½λλ₯Ό μ€μ ν©λλ€...".colorize(:yellow)
|
|
41
|
+
cloudflare.setup_dns_records(selected_domains, network.public_ip)
|
|
42
|
+
|
|
43
|
+
# 6. Welcome μλΉμ€ νμΈ
|
|
44
|
+
welcome = Tayo::Proxy::WelcomeService.new
|
|
45
|
+
welcome.ensure_running
|
|
46
|
+
|
|
47
|
+
# 7. Traefik μ€μ
|
|
48
|
+
traefik = Tayo::Proxy::TraefikConfig.new
|
|
49
|
+
traefik.setup(selected_domains)
|
|
50
|
+
|
|
51
|
+
# 8. μ΅μ’
μλ΄
|
|
52
|
+
show_summary(selected_domains, network)
|
|
53
|
+
|
|
54
|
+
rescue => e
|
|
55
|
+
puts "β μ€λ₯κ° λ°μνμ΅λλ€: #{e.message}".colorize(:red)
|
|
56
|
+
puts e.backtrace.join("\n") if ENV["DEBUG"]
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def show_summary(domains, network)
|
|
63
|
+
puts "\n" + "="*60
|
|
64
|
+
puts "β
Proxy μ€μ μ΄ μλ£λμμ΅λλ€!".colorize(:green)
|
|
65
|
+
puts "="*60
|
|
66
|
+
|
|
67
|
+
puts "\nπ μ€μ μμ½:".colorize(:yellow)
|
|
68
|
+
puts "β"*40
|
|
69
|
+
puts "κ³΅μΈ IP: #{network.public_ip}".colorize(:white)
|
|
70
|
+
puts "λ΄λΆ IP: #{network.internal_ip}".colorize(:white)
|
|
71
|
+
puts "Traefik: 80, 443 ν¬νΈ μ¬μ© μ€".colorize(:white)
|
|
72
|
+
puts "λμ보λ: http://localhost:8080".colorize(:white)
|
|
73
|
+
puts "β"*40
|
|
74
|
+
|
|
75
|
+
puts "\nπ νμ±νλ λλ©μΈ:".colorize(:yellow)
|
|
76
|
+
domains.each do |domain|
|
|
77
|
+
if network.use_custom_ports?
|
|
78
|
+
puts "β’ #{domain}".colorize(:cyan)
|
|
79
|
+
puts " HTTP: http://#{domain}:#{network.external_http}".colorize(:gray)
|
|
80
|
+
puts " HTTPS: https://#{domain}:#{network.external_https}".colorize(:gray)
|
|
81
|
+
else
|
|
82
|
+
puts "β’ #{domain}".colorize(:cyan)
|
|
83
|
+
puts " HTTP: http://#{domain}".colorize(:gray)
|
|
84
|
+
puts " HTTPS: https://#{domain}".colorize(:gray)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if network.use_custom_ports?
|
|
89
|
+
puts "\nπ‘ 곡μ κΈ° ν¬νΈν¬μλ©μ μ€μ νμΈμ!".colorize(:yellow)
|
|
90
|
+
network.show_port_forwarding_guide
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\nπ λͺ¨λ μ€μ μ΄ μλ£λμμ΅λλ€!".colorize(:green)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "json"
|
|
7
|
+
require "uri"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
|
|
10
|
+
module Tayo
|
|
11
|
+
module Proxy
|
|
12
|
+
class CloudflareClient
|
|
13
|
+
TOKEN_FILE = File.expand_path("~/.tayo/cloudflare_token")
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@prompt = TTY::Prompt.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ensure_token
|
|
20
|
+
@token = load_saved_token
|
|
21
|
+
|
|
22
|
+
if @token
|
|
23
|
+
puts "π μ μ₯λ Cloudflare ν ν°μ νμΈν©λλ€...".colorize(:yellow)
|
|
24
|
+
|
|
25
|
+
if test_token(@token)
|
|
26
|
+
puts "β
μ μ₯λ Cloudflare ν ν°μ΄ μ ν¨ν©λλ€.".colorize(:green)
|
|
27
|
+
else
|
|
28
|
+
puts "β οΈ μ μ₯λ ν ν°μ΄ λ§λ£λμκ±°λ μ ν¨νμ§ μμ΅λλ€.".colorize(:yellow)
|
|
29
|
+
puts "μ ν ν°μ μ
λ ₯ν΄μ£ΌμΈμ.".colorize(:cyan)
|
|
30
|
+
open_token_creation_page
|
|
31
|
+
@token = get_cloudflare_token
|
|
32
|
+
save_token(@token)
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
puts "\nπ Cloudflare API ν ν°μ΄ νμν©λλ€.".colorize(:yellow)
|
|
36
|
+
open_token_creation_page
|
|
37
|
+
@token = get_cloudflare_token
|
|
38
|
+
save_token(@token)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def select_domains
|
|
43
|
+
puts "\nπ Cloudflare λλ©μΈ λͺ©λ‘μ μ‘°νν©λλ€...".colorize(:yellow)
|
|
44
|
+
|
|
45
|
+
zones = get_cloudflare_zones
|
|
46
|
+
|
|
47
|
+
if zones.empty?
|
|
48
|
+
puts "β Cloudflareμ λ±λ‘λ λλ©μΈμ΄ μμ΅λλ€.".colorize(:red)
|
|
49
|
+
puts "λ¨Όμ https://dash.cloudflare.com μμ λλ©μΈμ μΆκ°ν΄μ£ΌμΈμ.".colorize(:cyan)
|
|
50
|
+
return []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
zone_choices = zones.map { |zone| "#{zone['name']} (#{zone['status']})" }
|
|
54
|
+
|
|
55
|
+
selected_zones = @prompt.multi_select(
|
|
56
|
+
"Caddyλ‘ λΌμ°ν
ν λλ©μΈμ μ ννμΈμ (Spaceλ‘ μ ν, Enterλ‘ μλ£):",
|
|
57
|
+
zone_choices,
|
|
58
|
+
per_page: 10,
|
|
59
|
+
min: 1
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# μ νλ νλͺ©μ΄ μμΌλ©΄ λΉ λ°°μ΄ λ°ν
|
|
63
|
+
return [] if selected_zones.nil? || selected_zones.empty?
|
|
64
|
+
|
|
65
|
+
puts "DEBUG: selected_zones = #{selected_zones.inspect}" if ENV["DEBUG"]
|
|
66
|
+
|
|
67
|
+
selected_domains = selected_zones.map do |selection|
|
|
68
|
+
zone_name = selection.split(' ').first
|
|
69
|
+
zone = zones.find { |z| z['name'] == zone_name }
|
|
70
|
+
|
|
71
|
+
# μλΈλλ©μΈ μΆκ° μ¬λΆ νμΈ
|
|
72
|
+
if @prompt.yes?("#{zone_name}μ μλΈλλ©μΈμ μΆκ°νμκ² μ΅λκΉ?")
|
|
73
|
+
subdomain = @prompt.ask("μλΈλλ©μΈμ μ
λ ₯νμΈμ (μ: app, api):")
|
|
74
|
+
if subdomain && !subdomain.empty?
|
|
75
|
+
{
|
|
76
|
+
domain: "#{subdomain}.#{zone_name}",
|
|
77
|
+
zone_id: zone['id'],
|
|
78
|
+
zone_name: zone_name
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
{
|
|
82
|
+
domain: zone_name,
|
|
83
|
+
zone_id: zone['id'],
|
|
84
|
+
zone_name: zone_name
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
{
|
|
89
|
+
domain: zone_name,
|
|
90
|
+
zone_id: zone['id'],
|
|
91
|
+
zone_name: zone_name
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "\nβ
μ νλ λλ©μΈ:".colorize(:green)
|
|
97
|
+
selected_domains.each do |d|
|
|
98
|
+
puts " β’ #{d[:domain]}".colorize(:cyan)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
puts "DEBUG: returning selected_domains = #{selected_domains.inspect}" if ENV["DEBUG"]
|
|
102
|
+
|
|
103
|
+
selected_domains
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def setup_dns_records(domains, public_ip)
|
|
107
|
+
domains.each do |domain_info|
|
|
108
|
+
domain = domain_info[:domain]
|
|
109
|
+
zone_id = domain_info[:zone_id]
|
|
110
|
+
|
|
111
|
+
puts "\nβοΈ #{domain}μ DNS λ μ½λλ₯Ό μ€μ ν©λλ€...".colorize(:yellow)
|
|
112
|
+
|
|
113
|
+
# κΈ°μ‘΄ A λ μ½λ νμΈ
|
|
114
|
+
existing_records = get_dns_records(zone_id, domain, ['A'])
|
|
115
|
+
|
|
116
|
+
if existing_records.any?
|
|
117
|
+
existing_record = existing_records.first
|
|
118
|
+
if existing_record['content'] == public_ip
|
|
119
|
+
puts "β
#{domain} β #{public_ip} (μ΄λ―Έ μ€μ λ¨)".colorize(:green)
|
|
120
|
+
else
|
|
121
|
+
puts "β οΈ κΈ°μ‘΄ λ μ½λλ₯Ό μ
λ°μ΄νΈν©λλ€.".colorize(:yellow)
|
|
122
|
+
update_dns_record(zone_id, existing_record['id'], public_ip)
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
# μ A λ μ½λ μμ±
|
|
126
|
+
create_dns_record(zone_id, domain, 'A', public_ip)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def load_saved_token
|
|
134
|
+
return nil unless File.exist?(TOKEN_FILE)
|
|
135
|
+
File.read(TOKEN_FILE).strip
|
|
136
|
+
rescue
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def save_token(token)
|
|
141
|
+
dir = File.dirname(TOKEN_FILE)
|
|
142
|
+
|
|
143
|
+
# λλ ν λ¦¬κ° νμΌλ‘ μ‘΄μ¬νλ κ²½μ° μμ
|
|
144
|
+
if File.exist?(dir) && !File.directory?(dir)
|
|
145
|
+
FileUtils.rm(dir)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# λλ ν λ¦¬κ° μμ λλ§ μμ±
|
|
149
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
150
|
+
|
|
151
|
+
File.write(TOKEN_FILE, token)
|
|
152
|
+
File.chmod(0600, TOKEN_FILE)
|
|
153
|
+
puts "β
API ν ν°μ΄ μ μ₯λμμ΅λλ€.".colorize(:green)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def open_token_creation_page
|
|
157
|
+
puts "ν ν° μμ± νμ΄μ§λ₯Ό μ½λλ€...".colorize(:cyan)
|
|
158
|
+
system("open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null || xdg-open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null")
|
|
159
|
+
|
|
160
|
+
puts "\nλ€μ κΆνμΌλ‘ ν ν°μ μμ±ν΄μ£ΌμΈμ:".colorize(:yellow)
|
|
161
|
+
puts ""
|
|
162
|
+
puts "νκ΅μ΄ νλ©΄:".colorize(:gray)
|
|
163
|
+
puts "β’ μμ β μμ β μ½κΈ°".colorize(:white)
|
|
164
|
+
puts "β’ μμ β DNS β νΈμ§".colorize(:white)
|
|
165
|
+
puts " (μμ 리μμ€λ 'λͺ¨λ μμ' μ ν)".colorize(:gray)
|
|
166
|
+
puts ""
|
|
167
|
+
puts "English:".colorize(:gray)
|
|
168
|
+
puts "β’ Zone β Zone β Read".colorize(:white)
|
|
169
|
+
puts "β’ Zone β DNS β Edit".colorize(:white)
|
|
170
|
+
puts " (Zone Resources: Select 'All zones')".colorize(:gray)
|
|
171
|
+
puts ""
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def get_cloudflare_token
|
|
175
|
+
token = @prompt.mask("μμ±λ Cloudflare API ν ν°μ λΆμ¬λ£μΌμΈμ:")
|
|
176
|
+
|
|
177
|
+
if token.nil? || token.strip.empty?
|
|
178
|
+
puts "β ν ν°μ΄ μ
λ ₯λμ§ μμμ΅λλ€.".colorize(:red)
|
|
179
|
+
exit 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if test_token(token.strip)
|
|
183
|
+
puts "β
ν ν°μ΄ νμΈλμμ΅λλ€.".colorize(:green)
|
|
184
|
+
return token.strip
|
|
185
|
+
else
|
|
186
|
+
puts "β ν ν°μ΄ μ¬λ°λ₯΄μ§ μκ±°λ κΆνμ΄ λΆμ‘±ν©λλ€.".colorize(:red)
|
|
187
|
+
exit 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_token(token)
|
|
192
|
+
uri = URI('https://api.cloudflare.com/client/v4/user/tokens/verify')
|
|
193
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
194
|
+
http.use_ssl = true
|
|
195
|
+
|
|
196
|
+
request = Net::HTTP::Get.new(uri)
|
|
197
|
+
request['Authorization'] = "Bearer #{token}"
|
|
198
|
+
request['Content-Type'] = 'application/json'
|
|
199
|
+
|
|
200
|
+
response = http.request(request)
|
|
201
|
+
|
|
202
|
+
if response.code == '200'
|
|
203
|
+
data = JSON.parse(response.body)
|
|
204
|
+
return data['success'] == true
|
|
205
|
+
else
|
|
206
|
+
return false
|
|
207
|
+
end
|
|
208
|
+
rescue => e
|
|
209
|
+
puts "β οΈ ν ν° κ²μ¦ μ€ μ€λ₯: #{e.message}".colorize(:yellow) if ENV["DEBUG"]
|
|
210
|
+
false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def get_cloudflare_zones
|
|
214
|
+
uri = URI('https://api.cloudflare.com/client/v4/zones')
|
|
215
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
216
|
+
http.use_ssl = true
|
|
217
|
+
|
|
218
|
+
request = Net::HTTP::Get.new(uri)
|
|
219
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
220
|
+
request['Content-Type'] = 'application/json'
|
|
221
|
+
|
|
222
|
+
response = http.request(request)
|
|
223
|
+
|
|
224
|
+
if response.code == '200'
|
|
225
|
+
data = JSON.parse(response.body)
|
|
226
|
+
return data['result'] || []
|
|
227
|
+
else
|
|
228
|
+
puts "β λλ©μΈ λͺ©λ‘ μ‘°νμ μ€ν¨νμ΅λλ€: #{response.code}".colorize(:red)
|
|
229
|
+
[]
|
|
230
|
+
end
|
|
231
|
+
rescue => e
|
|
232
|
+
puts "β API μμ² μ€ μ€λ₯κ° λ°μνμ΅λλ€: #{e.message}".colorize(:red)
|
|
233
|
+
[]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def get_dns_records(zone_id, name, types)
|
|
237
|
+
records = []
|
|
238
|
+
|
|
239
|
+
types.each do |type|
|
|
240
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
241
|
+
uri.query = URI.encode_www_form({
|
|
242
|
+
type: type,
|
|
243
|
+
name: name
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
247
|
+
http.use_ssl = true
|
|
248
|
+
|
|
249
|
+
request = Net::HTTP::Get.new(uri)
|
|
250
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
251
|
+
request['Content-Type'] = 'application/json'
|
|
252
|
+
|
|
253
|
+
response = http.request(request)
|
|
254
|
+
|
|
255
|
+
if response.code == '200'
|
|
256
|
+
data = JSON.parse(response.body)
|
|
257
|
+
records.concat(data['result'] || [])
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
records
|
|
262
|
+
rescue => e
|
|
263
|
+
puts "β οΈ DNS λ μ½λ μ‘°ν μ€ μ€λ₯: #{e.message}".colorize(:yellow)
|
|
264
|
+
[]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def create_dns_record(zone_id, name, type, content)
|
|
268
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
269
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
270
|
+
http.use_ssl = true
|
|
271
|
+
|
|
272
|
+
request = Net::HTTP::Post.new(uri)
|
|
273
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
274
|
+
request['Content-Type'] = 'application/json'
|
|
275
|
+
|
|
276
|
+
data = {
|
|
277
|
+
type: type,
|
|
278
|
+
name: name,
|
|
279
|
+
content: content,
|
|
280
|
+
ttl: 300,
|
|
281
|
+
proxied: false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
request.body = data.to_json
|
|
285
|
+
response = http.request(request)
|
|
286
|
+
|
|
287
|
+
if response.code == '200'
|
|
288
|
+
puts "β
#{name} β #{content} (A λ μ½λ μμ±λ¨)".colorize(:green)
|
|
289
|
+
else
|
|
290
|
+
puts "β DNS λ μ½λ μμ± μ€ν¨: #{response.code}".colorize(:red)
|
|
291
|
+
puts response.body if ENV["DEBUG"]
|
|
292
|
+
end
|
|
293
|
+
rescue => e
|
|
294
|
+
puts "β DNS λ μ½λ μμ± μ€ μ€λ₯: #{e.message}".colorize(:red)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def update_dns_record(zone_id, record_id, new_content)
|
|
298
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records/#{record_id}")
|
|
299
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
300
|
+
http.use_ssl = true
|
|
301
|
+
|
|
302
|
+
request = Net::HTTP::Patch.new(uri)
|
|
303
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
304
|
+
request['Content-Type'] = 'application/json'
|
|
305
|
+
|
|
306
|
+
data = {
|
|
307
|
+
content: new_content
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
request.body = data.to_json
|
|
311
|
+
response = http.request(request)
|
|
312
|
+
|
|
313
|
+
if response.code == '200'
|
|
314
|
+
puts "β
DNS λ μ½λκ° μ
λ°μ΄νΈλμμ΅λλ€.".colorize(:green)
|
|
315
|
+
else
|
|
316
|
+
puts "β DNS λ μ½λ μ
λ°μ΄νΈ μ€ν¨: #{response.code}".colorize(:red)
|
|
317
|
+
end
|
|
318
|
+
rescue => e
|
|
319
|
+
puts "β DNS λ μ½λ μ
λ°μ΄νΈ μ€ μ€λ₯: #{e.message}".colorize(:red)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
@@ -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
|