tayo 0.2.3 → 0.3.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.
@@ -1,303 +0,0 @@
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
@@ -1,337 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "colorize"
4
- require "fileutils"
5
-
6
- module Tayo
7
- module Proxy
8
- class WelcomeService
9
- def initialize
10
- @docker = DockerManager.new
11
- end
12
-
13
- def ensure_running
14
- # 호스트에서 3000 포트 서비스 확인 (Docker 제외)
15
- if host_port_in_use?(3000)
16
- puts "✅ 3000 포트에 호스트 서비스(Rails 등)가 실행 중입니다.".colorize(:green)
17
-
18
- # 기존 Welcome 컨테이너가 있다면 중지
19
- if @docker.container_running?("tayo-welcome")
20
- puts "🛑 기존 Welcome 서비스를 중지합니다...".colorize(:yellow)
21
- @docker.stop_container("tayo-welcome")
22
- end
23
- return
24
- end
25
-
26
- # Docker 컨테이너로 3000 포트가 사용 중인 경우
27
- if @docker.container_running?("tayo-welcome")
28
- puts "✅ Welcome 서비스가 이미 실행 중입니다.".colorize(:green)
29
- return
30
- end
31
-
32
- puts "\n🚀 3000 포트에 Welcome 서비스를 시작합니다...".colorize(:yellow)
33
- puts " (Rails 서버를 시작하면 자동으로 중지됩니다)".colorize(:gray)
34
-
35
- prepare_welcome_files
36
- build_image
37
- start_container
38
-
39
- puts "✅ Welcome 서비스가 시작되었습니다.".colorize(:green)
40
- end
41
-
42
- private
43
-
44
- def host_port_in_use?(port)
45
- # macOS와 Linux에서 호스트 포트 확인 (Docker 컨테이너 제외)
46
- if RUBY_PLATFORM.include?("darwin")
47
- # macOS: lsof 사용, Docker 프로세스 제외
48
- output = `lsof -i :#{port} -sTCP:LISTEN 2>/dev/null | grep -v docker | grep -v com.docke | grep -v tayo-welcome`.strip
49
- !output.empty?
50
- else
51
- # Linux: netstat 또는 ss 사용
52
- output = `netstat -tln 2>/dev/null | grep ":#{port} " | grep -v docker`.strip
53
- if output.empty?
54
- output = `ss -tln 2>/dev/null | grep ":#{port} " | grep -v docker`.strip
55
- end
56
- !output.empty?
57
- end
58
- end
59
-
60
- def template_dir
61
- File.expand_path("../../../templates/welcome", __FILE__)
62
- end
63
-
64
- def prepare_welcome_files
65
- puts "📁 Welcome 서비스 파일을 준비합니다...".colorize(:yellow)
66
-
67
- # 템플릿 디렉토리 생성
68
- FileUtils.mkdir_p(template_dir)
69
-
70
- # Dockerfile 생성
71
- create_dockerfile
72
-
73
- # index.html 생성
74
- create_index_html
75
- end
76
-
77
- def create_dockerfile
78
- dockerfile_content = <<~DOCKERFILE
79
- FROM nginx:alpine
80
-
81
- # Nginx 설정
82
- RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ =404; } }' > /etc/nginx/conf.d/default.conf
83
-
84
- # HTML 파일 복사
85
- COPY index.html /usr/share/nginx/html/
86
-
87
- # 헬스체크
88
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
89
- CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
90
-
91
- EXPOSE 80
92
-
93
- CMD ["nginx", "-g", "daemon off;"]
94
- DOCKERFILE
95
-
96
- File.write(File.join(template_dir, "Dockerfile"), dockerfile_content)
97
- end
98
-
99
- def create_index_html
100
- html_content = <<~HTML
101
- <!DOCTYPE html>
102
- <html lang="ko">
103
- <head>
104
- <meta charset="UTF-8">
105
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
106
- <title>Tayo Proxy - Welcome</title>
107
- <style>
108
- * {
109
- margin: 0;
110
- padding: 0;
111
- box-sizing: border-box;
112
- }
113
-
114
- body {
115
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
116
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
117
- min-height: 100vh;
118
- display: flex;
119
- align-items: center;
120
- justify-content: center;
121
- color: white;
122
- padding: 20px;
123
- }
124
-
125
- .container {
126
- text-align: center;
127
- max-width: 600px;
128
- animation: fadeIn 1s ease-in;
129
- }
130
-
131
- @keyframes fadeIn {
132
- from { opacity: 0; transform: translateY(-20px); }
133
- to { opacity: 1; transform: translateY(0); }
134
- }
135
-
136
- .logo {
137
- font-size: 5em;
138
- margin-bottom: 20px;
139
- animation: bounce 2s infinite;
140
- }
141
-
142
- @keyframes bounce {
143
- 0%, 100% { transform: translateY(0); }
144
- 50% { transform: translateY(-10px); }
145
- }
146
-
147
- h1 {
148
- font-size: 3em;
149
- margin-bottom: 20px;
150
- font-weight: 700;
151
- text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
152
- }
153
-
154
- .subtitle {
155
- font-size: 1.3em;
156
- opacity: 0.95;
157
- margin-bottom: 30px;
158
- line-height: 1.5;
159
- }
160
-
161
- .status {
162
- background: rgba(255, 255, 255, 0.2);
163
- border-radius: 10px;
164
- padding: 20px;
165
- backdrop-filter: blur(10px);
166
- margin-top: 30px;
167
- }
168
-
169
- .status-item {
170
- display: flex;
171
- justify-content: space-between;
172
- padding: 10px 0;
173
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
174
- }
175
-
176
- .status-item:last-child {
177
- border-bottom: none;
178
- }
179
-
180
- .status-label {
181
- font-weight: 600;
182
- }
183
-
184
- .status-value {
185
- opacity: 0.9;
186
- }
187
-
188
- .status-ok {
189
- color: #4ade80;
190
- }
191
-
192
- .info-box {
193
- background: rgba(255, 255, 255, 0.1);
194
- border-radius: 8px;
195
- padding: 15px;
196
- margin-top: 30px;
197
- font-size: 0.9em;
198
- opacity: 0.85;
199
- }
200
-
201
- .footer {
202
- margin-top: 50px;
203
- font-size: 0.85em;
204
- opacity: 0.7;
205
- }
206
-
207
- .footer a {
208
- color: white;
209
- text-decoration: none;
210
- border-bottom: 1px solid rgba(255, 255, 255, 0.3);
211
- transition: border-color 0.3s;
212
- }
213
-
214
- .footer a:hover {
215
- border-bottom-color: white;
216
- }
217
- </style>
218
- </head>
219
- <body>
220
- <div class="container">
221
- <div class="logo">🚀</div>
222
- <h1>Tayo Proxy</h1>
223
- <p class="subtitle">
224
- 홈서버 프록시 서비스가 정상적으로 작동 중입니다<br>
225
- Your home server proxy is running successfully
226
- </p>
227
-
228
- <div class="status">
229
- <div class="status-item">
230
- <span class="status-label">프록시 상태</span>
231
- <span class="status-value status-ok">✓ 활성</span>
232
- </div>
233
- <div class="status-item">
234
- <span class="status-label">Kamal Proxy</span>
235
- <span class="status-value status-ok">✓ 실행 중</span>
236
- </div>
237
- <div class="status-item">
238
- <span class="status-label">Caddy Server</span>
239
- <span class="status-value status-ok">✓ 실행 중</span>
240
- </div>
241
- <div class="status-item">
242
- <span class="status-label">SSL/TLS</span>
243
- <span class="status-value status-ok">✓ 준비됨</span>
244
- </div>
245
- </div>
246
-
247
- <div class="info-box">
248
- <strong>💡 다음 단계:</strong><br>
249
- 이제 실제 애플리케이션을 3000 포트에 배포하면<br>
250
- 이 페이지 대신 애플리케이션이 표시됩니다.
251
- </div>
252
-
253
- <div class="footer">
254
- <p>
255
- Powered by <a href="https://github.com/TeamMilestone/tayo" target="_blank">Tayo</a> |
256
- <a href="https://kamal-deploy.org" target="_blank">Kamal</a> |
257
- <a href="https://caddyserver.com" target="_blank">Caddy</a>
258
- </p>
259
- </div>
260
- </div>
261
-
262
- <script>
263
- // 현재 시간 표시 (옵션)
264
- const updateTime = () => {
265
- const now = new Date();
266
- const timeString = now.toLocaleTimeString('ko-KR');
267
- // 시간 표시 기능이 필요한 경우 활성화
268
- };
269
- setInterval(updateTime, 1000);
270
- updateTime();
271
- </script>
272
- </body>
273
- </html>
274
- HTML
275
-
276
- File.write(File.join(template_dir, "index.html"), html_content)
277
- end
278
-
279
- def build_image
280
- puts "🔨 Docker 이미지를 빌드합니다...".colorize(:yellow)
281
-
282
- cmd = "docker build -t tayo-welcome:latest #{template_dir}"
283
-
284
- if system(cmd)
285
- puts "✅ Docker 이미지 빌드가 완료되었습니다.".colorize(:green)
286
- else
287
- puts "❌ Docker 이미지 빌드에 실패했습니다.".colorize(:red)
288
- exit 1
289
- end
290
- end
291
-
292
- def start_container
293
- puts "🚀 Welcome 컨테이너를 시작합니다...".colorize(:yellow)
294
-
295
- # 기존 컨테이너가 있다면 제거
296
- if @docker.container_exists?("tayo-welcome")
297
- @docker.stop_container("tayo-welcome")
298
- end
299
-
300
- # 네트워크 확인
301
- network = @docker.create_network_if_not_exists("tayo-proxy")
302
-
303
- # Welcome 컨테이너 실행
304
- cmd = <<~DOCKER
305
- docker run -d \
306
- --name tayo-welcome \
307
- --network #{network} \
308
- -p 3000:80 \
309
- --restart unless-stopped \
310
- tayo-welcome:latest
311
- DOCKER
312
-
313
- if system(cmd)
314
- puts "✅ Welcome 서비스가 포트 3000에서 시작되었습니다.".colorize(:green)
315
-
316
- # 서비스 확인
317
- sleep 2
318
- check_service_health
319
- else
320
- puts "❌ Welcome 서비스 시작에 실패했습니다.".colorize(:red)
321
- exit 1
322
- end
323
- end
324
-
325
- def check_service_health
326
- # curl로 서비스 확인
327
- response = `curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null`.strip
328
-
329
- if response == "200"
330
- puts "✅ Welcome 서비스가 정상적으로 응답합니다.".colorize(:green)
331
- else
332
- puts "⚠️ Welcome 서비스 응답 확인 중... (HTTP #{response})".colorize(:yellow)
333
- end
334
- end
335
- end
336
- end
337
- end
@@ -1,14 +0,0 @@
1
- FROM nginx:alpine
2
-
3
- # Nginx 설정
4
- RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ =404; } }' > /etc/nginx/conf.d/default.conf
5
-
6
- # HTML 파일 복사
7
- COPY index.html /usr/share/nginx/html/
8
-
9
- # 헬스체크
10
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
11
-
12
- EXPOSE 80
13
-
14
- CMD ["nginx", "-g", "daemon off;"]