feat(TALE-7): reimplement integration system

This commit is contained in:
Aarnav Tale 2024-07-09 22:53:00 -04:00
parent 3cc726320a
commit 0aa0406ea6
No known key found for this signature in database
6 changed files with 356 additions and 375 deletions

View file

@ -5,123 +5,143 @@ import { Client } from 'undici'
import { HeadscaleError, pull } from '~/utils/headscale'
import type { Integration } from '.'
import { createIntegration } from './integration'
// Integration name
const name = 'Docker'
interface Context {
client: Client | undefined
container: string | undefined
maxAttempts: number
}
let url: URL | undefined
let container: string | undefined
export default createIntegration<Context>({
name: 'Docker',
context: {
client: undefined,
container: undefined,
maxAttempts: 10,
},
isAvailable: async ({ client, container }) => {
// Check for the HEADSCALE_CONTAINER environment variable first
// to avoid unnecessary fetching of the Docker socket
container = process.env.HEADSCALE_CONTAINER
?.trim()
.toLowerCase()
async function preflight() {
const path = process.env.DOCKER_SOCK ?? 'unix:///var/run/docker.sock'
if (!container || container.length === 0) {
return false
}
try {
url = new URL(path)
} catch {
return false
}
const path = process.env.DOCKER_SOCK ?? 'unix:///var/run/docker.sock'
let url: URL | undefined
// The API is available as an HTTP endpoint
if (url.protocol === 'tcp:') {
url.protocol = 'http:'
}
// Check if the socket is accessible
if (url.protocol === 'unix:') {
try {
await access(path, constants.R_OK)
url = new URL(path)
} catch {
return false
}
}
if (url.protocol === 'http:') {
try {
await fetch(new URL('/v1.30/version', url).href)
} catch {
if (url.protocol !== 'tcp:' && url.protocol !== 'unix:') {
return false
}
}
if (url.protocol !== 'http:' && url.protocol !== 'unix:') {
return false
}
// The API is available as an HTTP endpoint and this
// will simplify the fetching logic in undici
if (url.protocol === 'tcp:') {
url.protocol = 'http:'
try {
await fetch(new URL('/v1.30/version', url).href)
} catch {
return false
}
container = process.env.HEADSCALE_CONTAINER
?.trim()
.toLowerCase()
client = new Client(url.href)
}
if (!container || container.length === 0) {
return false
}
// Check if the socket is accessible
if (url.protocol === 'unix:') {
try {
await access(path, constants.R_OK)
} catch {
return false
}
return true
}
client = new Client('http://localhost', {
socketPath: path,
})
}
async function sighup() {
if (!url || !container) {
return
}
return client === undefined
},
// Supports the DOCKER_SOCK environment variable
const client = url.protocol === 'unix:'
? new Client('http://localhost', {
socketPath: url.href,
})
: new Client(url.href)
const response = await client.request({
method: 'POST',
path: `/v1.30/containers/${container}/kill?signal=SIGHUP`,
})
if (!response.statusCode || response.statusCode !== 204) {
throw new Error('Failed to send SIGHUP to Headscale')
}
}
async function restart() {
if (!url || !container) {
return
}
// Supports the DOCKER_SOCK environment variable
const client = url.protocol === 'unix:'
? new Client('http://localhost', {
socketPath: url.href,
})
: new Client(url.href)
const response = await client.request({
method: 'POST',
path: `/v1.30/containers/${container}/restart`,
})
if (!response.statusCode || response.statusCode !== 204) {
throw new Error('Failed to restart Headscale')
}
// Wait for Headscale to restart before continuing
let attempts = 0
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
while (true) {
try {
await pull('v1', '')
onAclChange: async ({ client, container, maxAttempts }) => {
if (!client || !container) {
return
} catch (error) {
if (error instanceof HeadscaleError && error.status === 401) {
break
}
if (attempts > 10) {
throw new Error('Headscale did not restart in time')
}
attempts++
await setTimeout(1000)
}
}
}
export default { name, preflight, sighup, restart } satisfies Integration
let attempts = 0
while (attempts <= maxAttempts) {
const response = await client.request({
method: 'POST',
path: `/v1.30/containers/${container}/kill?signal=SIGHUP`,
})
if (response.statusCode !== 204) {
if (attempts < maxAttempts) {
attempts++
await setTimeout(1000)
continue
}
const stringCode = response.statusCode.toString()
const body = await response.body.text()
throw new Error(`API request failed: ${stringCode} ${body}`)
}
}
},
onConfigChange: async ({ client, container, maxAttempts }) => {
if (!client || !container) {
return
}
let attempts = 0
while (attempts <= maxAttempts) {
const response = await client.request({
method: 'POST',
path: `/v1.30/containers/${container}/restart`,
})
if (response.statusCode !== 204) {
if (attempts < maxAttempts) {
attempts++
await setTimeout(1000)
continue
}
const stringCode = response.statusCode.toString()
const body = await response.body.text()
throw new Error(`API request failed: ${stringCode} ${body}`)
}
}
attempts = 0
while (attempts <= maxAttempts) {
try {
await pull('v1', '')
return
} catch (error) {
if (error instanceof HeadscaleError && error.status === 401) {
break
}
if (attempts < maxAttempts) {
attempts++
await setTimeout(1000)
continue
}
throw new Error(`Missed restart deadline for ${container}`)
}
}
},
})