Skip to content

Commit ede65e4

Browse files
committed
feat :: base commit
1 parent 26571a2 commit ede65e4

62 files changed

Lines changed: 9580 additions & 132 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Dependencies
2+
node_modules
3+
npm-debug.log*
4+
yarn-debug.log*
5+
yarn-error.log*
6+
pnpm-debug.log*
7+
.pnpm-debug.log*
8+
9+
# Testing
10+
coverage
11+
*.lcov
12+
13+
# Next.js
14+
.next/
15+
out/
16+
build/
17+
dist/
18+
19+
# Production
20+
.vercel
21+
.env*.local
22+
.env.local
23+
.env.development.local
24+
.env.test.local
25+
.env.production.local
26+
27+
# Runtime data
28+
pids
29+
*.pid
30+
*.seed
31+
*.pid.lock
32+
33+
# Logs
34+
logs
35+
*.log
36+
37+
# IDE
38+
.vscode/
39+
.idea/
40+
*.swp
41+
*.swo
42+
*~
43+
44+
# OS
45+
.DS_Store
46+
.DS_Store?
47+
._*
48+
.Spotlight-V100
49+
.Trashes
50+
ehthumbs.db
51+
Thumbs.db
52+
53+
# Git
54+
.git/
55+
.gitignore
56+
57+
# Misc
58+
*.tgz
59+
*.tar.gz
60+
README.md
61+
CLAUDE.md
62+
docker-compose*.yml
63+
Dockerfile*
64+
helm/
65+
k8s/

.eslintrc.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "next/core-web-vitals",
3+
"rules": {
4+
"@typescript-eslint/no-explicit-any": "off",
5+
"@typescript-eslint/no-unused-vars": "off",
6+
"@typescript-eslint/no-require-imports": "off",
7+
"react/no-unescaped-entities": "off",
8+
"@next/next/no-img-element": "off"
9+
}
10+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
# misc
2424
.DS_Store
2525
*.pem
26+
*.backup
27+
*.old
28+
*.tmp
2629

2730
# debug
2831
npm-debug.log*

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Multi-stage build for production
2+
FROM node:18-alpine AS base
3+
4+
# Install Python and build tools for native dependencies
5+
RUN apk add --no-cache python3 make g++
6+
7+
# Install dependencies only when needed
8+
FROM base AS deps
9+
WORKDIR /app
10+
11+
# Install dependencies based on the preferred package manager
12+
COPY package.json package-lock.json* ./
13+
RUN npm ci --only=production --prefer-offline && npm cache clean --force
14+
15+
# Build the application
16+
FROM base AS builder
17+
WORKDIR /app
18+
COPY package.json package-lock.json* ./
19+
RUN npm ci && npm cache clean --force
20+
COPY . .
21+
22+
# Build the application
23+
ENV NODE_ENV=production
24+
ENV NEXT_TELEMETRY_DISABLED=1
25+
RUN npm run build
26+
27+
# Production image
28+
FROM node:18-alpine AS runner
29+
WORKDIR /app
30+
31+
ENV NODE_ENV=production
32+
ENV NEXT_TELEMETRY_DISABLED=1
33+
34+
# Create a non-root user
35+
RUN addgroup --system --gid 1001 nodejs
36+
RUN adduser --system --uid 1001 nextjs
37+
38+
# Copy built application
39+
COPY --from=builder /app/public ./public
40+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
41+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
42+
43+
44+
USER nextjs
45+
46+
EXPOSE 3000
47+
48+
ENV PORT=3000
49+
ENV HOSTNAME="0.0.0.0"
50+
51+
CMD ["node", "server.js"]

app/admin/[...service]/route.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getServiceUrls } from '../../lib/k8sClient';
3+
4+
const ALLOWED_SERVICES = ['grafana', 'kiali', 'jaeger', 'argocd', 'prometheus', 'alertmanager'];
5+
const REQUEST_TIMEOUT = 10000; // 10 seconds
6+
7+
export async function GET(request: NextRequest, context: { params: Promise<{ service: string[] }> }) {
8+
const params = await context.params;
9+
return handleServiceRequest(request, params.service, 'GET');
10+
}
11+
12+
export async function POST(request: NextRequest, context: { params: Promise<{ service: string[] }> }) {
13+
const params = await context.params;
14+
return handleServiceRequest(request, params.service, 'POST');
15+
}
16+
17+
export async function PUT(request: NextRequest, context: { params: Promise<{ service: string[] }> }) {
18+
const params = await context.params;
19+
return handleServiceRequest(request, params.service, 'PUT');
20+
}
21+
22+
export async function DELETE(request: NextRequest, context: { params: Promise<{ service: string[] }> }) {
23+
const params = await context.params;
24+
return handleServiceRequest(request, params.service, 'DELETE');
25+
}
26+
27+
async function handleServiceRequest(
28+
request: NextRequest,
29+
serviceSegments: string[],
30+
method: string
31+
) {
32+
try {
33+
const [service, ...pathSegments] = serviceSegments;
34+
35+
// Validate service
36+
if (!ALLOWED_SERVICES.includes(service)) {
37+
return NextResponse.json(
38+
{ error: 'Service not allowed' },
39+
{ status: 403 }
40+
);
41+
}
42+
43+
// Get service URLs
44+
const serviceUrls = getServiceUrls();
45+
const baseUrl = serviceUrls[service as keyof typeof serviceUrls];
46+
47+
if (!baseUrl) {
48+
return NextResponse.json(
49+
{ error: 'Service URL not found' },
50+
{ status: 404 }
51+
);
52+
}
53+
54+
// Build target URL
55+
const targetPath = pathSegments.length > 0 ? pathSegments.join('/') : '';
56+
const url = new URL(request.url);
57+
const queryString = url.searchParams.toString();
58+
const targetUrl = `${baseUrl}/${targetPath}${queryString ? `?${queryString}` : ''}`;
59+
60+
// Prepare headers and pass through authentication
61+
const headers = new Headers();
62+
headers.set('User-Agent', 'WindeathAdmin/1.0');
63+
headers.set('Accept', request.headers.get('accept') || 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
64+
65+
if (request.headers.get('content-type')) {
66+
headers.set('Content-Type', request.headers.get('content-type')!);
67+
}
68+
69+
// Pass through cookies for authentication
70+
if (request.headers.get('cookie')) {
71+
headers.set('Cookie', request.headers.get('cookie')!);
72+
}
73+
74+
// Pass through authorization header if present
75+
if (request.headers.get('authorization')) {
76+
headers.set('Authorization', request.headers.get('authorization')!);
77+
}
78+
79+
// Prepare request body for POST/PUT
80+
let body: string | undefined;
81+
if (method === 'POST' || method === 'PUT') {
82+
body = await request.text();
83+
}
84+
85+
// Create AbortController for timeout
86+
const controller = new AbortController();
87+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
88+
89+
try {
90+
// Make the request
91+
const response = await fetch(targetUrl, {
92+
method,
93+
headers,
94+
body,
95+
signal: controller.signal,
96+
});
97+
98+
clearTimeout(timeoutId);
99+
100+
// Handle different content types
101+
const contentType = response.headers.get('content-type');
102+
103+
if (contentType?.includes('text/html')) {
104+
// For HTML responses (like dashboard pages), return as-is but modify any relative URLs
105+
let html = await response.text();
106+
107+
// Replace relative URLs with admin-prefixed URLs for proper routing
108+
html = html.replace(/href="\/(?!admin)/g, `href="/admin/${service}/`);
109+
html = html.replace(/src="\/(?!admin)/g, `src="/admin/${service}/`);
110+
html = html.replace(/action="\/(?!admin)/g, `action="/admin/${service}/`);
111+
112+
return new NextResponse(html, {
113+
status: response.status,
114+
headers: {
115+
'Content-Type': contentType,
116+
'Cache-Control': 'no-cache, no-store, must-revalidate',
117+
},
118+
});
119+
} else if (contentType?.includes('application/json')) {
120+
const data = await response.json();
121+
return NextResponse.json(data, {
122+
status: response.status,
123+
headers: {
124+
'Content-Type': 'application/json',
125+
'Cache-Control': 'no-cache, no-store, must-revalidate',
126+
},
127+
});
128+
} else if (contentType?.includes('text/')) {
129+
const text = await response.text();
130+
return new NextResponse(text, {
131+
status: response.status,
132+
headers: {
133+
'Content-Type': contentType,
134+
'Cache-Control': 'no-cache, no-store, must-revalidate',
135+
},
136+
});
137+
} else {
138+
// Handle binary data (CSS, JS, images, etc.)
139+
const buffer = await response.arrayBuffer();
140+
return new NextResponse(buffer, {
141+
status: response.status,
142+
headers: {
143+
'Content-Type': contentType || 'application/octet-stream',
144+
'Cache-Control': 'no-cache, no-store, must-revalidate',
145+
},
146+
});
147+
}
148+
} catch (fetchError) {
149+
clearTimeout(timeoutId);
150+
151+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
152+
return NextResponse.json(
153+
{ error: 'Request timeout' },
154+
{ status: 504 }
155+
);
156+
}
157+
throw fetchError;
158+
}
159+
} catch (error) {
160+
console.error('Service proxy request failed:', error);
161+
162+
return NextResponse.json(
163+
{
164+
error: 'Service proxy request failed',
165+
details: error instanceof Error ? error.message : 'Unknown error',
166+
service: serviceSegments[0],
167+
},
168+
{ status: 500 }
169+
);
170+
}
171+
}
172+
173+
// Health check endpoint
174+
export async function HEAD(request: NextRequest, context: { params: Promise<{ service: string[] }> }) {
175+
const params = await context.params;
176+
try {
177+
const [service] = params.service;
178+
179+
if (!ALLOWED_SERVICES.includes(service)) {
180+
return new NextResponse(null, { status: 403 });
181+
}
182+
183+
const serviceUrls = getServiceUrls();
184+
const baseUrl = serviceUrls[service as keyof typeof serviceUrls];
185+
186+
if (!baseUrl) {
187+
return new NextResponse(null, { status: 404 });
188+
}
189+
190+
// Simple connectivity check
191+
const controller = new AbortController();
192+
const timeoutId = setTimeout(() => controller.abort(), 5000);
193+
194+
try {
195+
const response = await fetch(baseUrl, {
196+
method: 'HEAD',
197+
signal: controller.signal,
198+
});
199+
200+
clearTimeout(timeoutId);
201+
202+
return new NextResponse(null, {
203+
status: response.ok ? 200 : response.status,
204+
});
205+
} catch (fetchError) {
206+
clearTimeout(timeoutId);
207+
return new NextResponse(null, { status: 503 });
208+
}
209+
} catch (error) {
210+
console.error('Health check failed:', error);
211+
return new NextResponse(null, { status: 500 });
212+
}
213+
}

0 commit comments

Comments
 (0)