Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/datadog

src/smoke-test.ts

174 lines · 4.9 KB · TypeScript
import { kody } from 'kody:runtime'
import {
	API_KEY_SETUP_URL,
	APPLICATION_KEY_SETUP_URL,
	DATADOG_API_HOSTS,
	resolveApiHost,
	resolveApiKeySecretName,
	resolveApplicationKeySecretName,
	secretSetupUrl,
} from './core.ts'
import {
	createDashboard,
	deleteDashboard,
	listDashboards,
	updateDashboard,
} from './dashboards.ts'
import { createIncident, deleteIncident, updateIncident } from './incidents.ts'
import {
	createMonitor,
	deleteMonitor,
	listMonitors,
	muteMonitor,
	updateMonitor,
} from './monitors.ts'
import { validateKeys } from './validate.ts'
import type { DatadogSmokeTestInput } from './types.ts'

function secretEntries(result: unknown): Array<{ name?: string; scope?: string }> {
	if (result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets)) {
		return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets
	}
	return Array.isArray(result) ? result : []
}

async function listUserSecretNames(): Promise<Set<string>> {
	try {
		const listed = await kody.secret_list({ scope: 'user' })
		const names = new Set<string>()
		for (const entry of secretEntries(listed)) {
			if (entry?.name && (entry.scope === 'user' || !entry.scope)) names.add(entry.name)
		}
		return names
	} catch {
		return new Set()
	}
}

async function mutationSelfCheck() {
	const monitor = {
		monitorId: 1,
		type: 'query alert',
		query: 'avg(last_5m):avg:system.cpu.user{*} > 95',
		name: 'smoke-preview',
		message: 'dry-run only',
	}
	const create = await createMonitor({ ...monitor, dryRun: true })
	const update = await updateMonitor({ ...monitor, dryRun: true })
	const mute = await muteMonitor({ monitorId: 1, dryRun: true })
	const del = await deleteMonitor({ monitorId: 1, dryRun: true })
	const incident = await createIncident({ title: 'smoke-preview', customerImpacted: false, dryRun: true })
	const incidentUpdate = await updateIncident({
		incidentId: 'incident-smoke',
		title: 'smoke-preview',
		dryRun: true,
	})
	const incidentDelete = await deleteIncident({ incidentId: 'incident-smoke', dryRun: true })
	const dashboard = await createDashboard({
		title: 'smoke-preview',
		layoutType: 'ordered',
		widgets: [],
		dryRun: true,
	})
	const dashboardUpdate = await updateDashboard({
		dashboardId: 'abc-def',
		title: 'smoke-preview',
		layoutType: 'ordered',
		widgets: [],
		dryRun: true,
	})
	const dashboardDelete = await deleteDashboard({ dashboardId: 'abc-def', dryRun: true })
	const checks = [
		create,
		update,
		mute,
		del,
		incident,
		incidentUpdate,
		incidentDelete,
		dashboard,
		dashboardUpdate,
		dashboardDelete,
	]
	for (const check of checks) {
		if (!('dryRun' in check) || check.dryRun !== true) {
			throw new Error('Datadog dryRun mutation self-check failed.')
		}
	}
	return {
		monitors: true,
		incidents: true,
		dashboards: true,
	}
}

/**
 * Local dry-run checks plus an optional live key validation and sample reads.
 *
 * Never mutates monitors, incidents, or dashboards.
 *
 * @example
 * import smokeTest from 'kody:@kody/datadog/smoke-test'
 * const result = await smokeTest()
 */
export async function smokeTest(input: DatadogSmokeTestInput = {}) {
	const dryRunMutations = await mutationSelfCheck()
	const apiKeySecret = resolveApiKeySecretName(input)
	const applicationKeySecret = resolveApplicationKeySecretName(input)
	const names = await listUserSecretNames()
	const hasApiKey = names.has(apiKeySecret)
	const hasApplicationKey = names.has(applicationKeySecret)
	const host = resolveApiHost(input)

	if (!hasApiKey || !hasApplicationKey) {
		return {
			ok: true,
			live: false,
			selfCheck: { dryRunMutations },
			secrets: { apiKeySecret, applicationKeySecret, hasApiKey, hasApplicationKey },
			setup: {
				auth: 'api-key-and-application-key',
				hosts: [...DATADOG_API_HOSTS],
				site: input.site ?? 'us1',
				host,
				nextSteps: [
					'Create an API key and application key in Datadog Organization Settings.',
					hasApiKey ? null : secretSetupUrl(apiKeySecret),
					hasApplicationKey ? null : secretSetupUrl(applicationKeySecret),
					'Approve the Datadog API hosts for those secrets.',
				].filter(Boolean),
			},
		}
	}

	const validation = await validateKeys(input)
	const monitors =
		input.includeMonitors === false
			? null
			: await listMonitors({ ...input, pageSize: 5 })
	const dashboards =
		input.includeDashboards === false
			? null
			: await listDashboards({ ...input, count: 5 })

	return {
		ok: true,
		live: true,
		selfCheck: { dryRunMutations },
		secrets: { apiKeySecret, applicationKeySecret, hasApiKey, hasApplicationKey },
		validation,
		monitorCount: monitors?.count ?? null,
		monitorsSample: monitors?.monitors.slice(0, 5) ?? [],
		dashboardCount: dashboards?.count ?? null,
		dashboardsSample: dashboards?.dashboards.slice(0, 5) ?? [],
		setup: {
			auth: 'api-key-and-application-key',
			hosts: [...DATADOG_API_HOSTS],
			host,
			apiKeyUrl: API_KEY_SETUP_URL,
			applicationKeyUrl: APPLICATION_KEY_SETUP_URL,
		},
	}
}

export default smokeTest