117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
|
|
|
const corsHeaders = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
};
|
|
|
|
const DOCKER_SOCKET = '/var/run/docker.sock';
|
|
|
|
serve(async (req) => {
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const authHeader = req.headers.get('Authorization');
|
|
if (!authHeader) {
|
|
throw new Error('Missing authorization header');
|
|
}
|
|
|
|
const supabase = createClient(
|
|
Deno.env.get('SUPABASE_URL') ?? '',
|
|
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
|
{ global: { headers: { Authorization: authHeader } } }
|
|
);
|
|
|
|
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
|
if (userError || !user) {
|
|
throw new Error('Unauthorized');
|
|
}
|
|
|
|
const url = new URL(req.url);
|
|
const containerId = url.searchParams.get('container');
|
|
const tail = url.searchParams.get('tail') || '100';
|
|
|
|
if (!containerId) {
|
|
throw new Error('Container ID required');
|
|
}
|
|
|
|
// Connect to Docker socket
|
|
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
|
|
|
|
const request = `GET /containers/${containerId}/logs?stdout=true&stderr=true&tail=${tail}&follow=true HTTP/1.1\r\nHost: localhost\r\n\r\n`;
|
|
await conn.write(new TextEncoder().encode(request));
|
|
|
|
// Create a readable stream from the socket
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
const decoder = new TextDecoder();
|
|
let buffer = new Uint8Array(8192);
|
|
let headerRead = false;
|
|
|
|
try {
|
|
while (true) {
|
|
const n = await conn.read(buffer);
|
|
if (n === null) break;
|
|
|
|
let data = decoder.decode(buffer.subarray(0, n));
|
|
|
|
// Skip HTTP headers on first read
|
|
if (!headerRead) {
|
|
const headerEnd = data.indexOf('\r\n\r\n');
|
|
if (headerEnd !== -1) {
|
|
data = data.substring(headerEnd + 4);
|
|
headerRead = true;
|
|
}
|
|
}
|
|
|
|
// Docker logs use a special frame format:
|
|
// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
|
|
// We'll parse and clean this
|
|
const lines = data.split('\n').filter(line => {
|
|
// Skip binary headers (first 8 bytes of each frame)
|
|
if (line.length > 8) {
|
|
return line.substring(8).trim().length > 0;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
for (const line of lines) {
|
|
if (line.length > 8) {
|
|
const cleaned = line.substring(8).trim();
|
|
if (cleaned) {
|
|
controller.enqueue(new TextEncoder().encode(`data: ${cleaned}\n\n`));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Stream error:', error);
|
|
} finally {
|
|
conn.close();
|
|
controller.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
},
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Docker logs error:', error);
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
return new Response(JSON.stringify({ error: message }), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
});
|