177 lines
6.8 KiB
TypeScript
177 lines
6.8 KiB
TypeScript
import {afterEach, describe, expect, test} from 'bun:test'
|
|
import {AzureAksAdapter} from './AzureAksAdapter'
|
|
import {AzureRestRuntimeClient} from '../azure'
|
|
import {NotSupportedError, ValidationError} from '../cloud-spi/errors'
|
|
|
|
const originalFetch = globalThis.fetch
|
|
const ENDPOINT = 'http://localhost:4577'
|
|
const SUB = '00000000-0000-0000-0000-000000000001'
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch
|
|
})
|
|
|
|
function adapter(): AzureAksAdapter {
|
|
return new AzureAksAdapter(new AzureRestRuntimeClient(ENDPOINT, 'devstoreaccount1'))
|
|
}
|
|
|
|
function stubFetch(handler: (url: string, init?: RequestInit) => Response) {
|
|
const calls: Array<{url: string; init?: RequestInit}> = []
|
|
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
calls.push({url: String(url), init})
|
|
return handler(String(url), init)
|
|
}) as unknown as typeof fetch
|
|
return calls
|
|
}
|
|
|
|
function json(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {status})
|
|
}
|
|
|
|
function cluster(name: string, rg = 'rg-app', provisioningState = 'Succeeded') {
|
|
return {
|
|
id: `/subscriptions/${SUB}/resourceGroups/${rg}/providers/Microsoft.ContainerService/managedClusters/${name}`,
|
|
name,
|
|
type: 'Microsoft.ContainerService/managedClusters',
|
|
location: 'eastus',
|
|
properties: {
|
|
provisioningState,
|
|
kubernetesVersion: '1.29.0',
|
|
currentKubernetesVersion: '1.29.0',
|
|
dnsPrefix: `${name}dns`,
|
|
fqdn: `${name}dns.hcp.eastus.azmk8s.io`,
|
|
enableRBAC: true,
|
|
nodeResourceGroup: `MC_${rg}_${name}_eastus`,
|
|
agentPoolProfiles: [
|
|
{name: 'nodepool1', count: 2, vmSize: 'Standard_D2s_v3', osType: 'Linux', mode: 'System'},
|
|
],
|
|
},
|
|
}
|
|
}
|
|
|
|
function runtimeStub(clusters: unknown[] = [cluster('prod')]) {
|
|
return stubFetch((url) => {
|
|
if (url.endsWith('/subscriptions')) return json({value: [{subscriptionId: SUB}]})
|
|
if (url.includes('/managedClusters/')) return json(clusters[0] ?? {})
|
|
return json({value: clusters})
|
|
})
|
|
}
|
|
|
|
describe('AzureAksAdapter', () => {
|
|
test('identifies itself as the Azure k8s adapter', () => {
|
|
const a = adapter()
|
|
|
|
expect(a.cloud).toBe('azure')
|
|
expect(a.service).toBe('k8s')
|
|
expect(a.schema().displayName).toBe('Azure AKS')
|
|
})
|
|
|
|
test('lists clusters and maps the ARM shape onto the shared columns', async () => {
|
|
runtimeStub()
|
|
const [resource] = await adapter().list()
|
|
|
|
expect(resource).toMatchObject({
|
|
// Same addressing as Azure VMs: ARM cannot reach a cluster without its
|
|
// resource group, and the generic route passes one id.
|
|
id: 'rg-app/prod',
|
|
name: 'prod',
|
|
cloud: 'azure',
|
|
service: 'k8s',
|
|
type: 'cluster',
|
|
region: 'eastus',
|
|
status: 'Succeeded',
|
|
version: '1.29.0',
|
|
})
|
|
expect(resource?.metadata).toMatchObject({
|
|
resourceGroup: 'rg-app',
|
|
fqdn: 'proddns.hcp.eastus.azmk8s.io',
|
|
nodeResourceGroup: 'MC_rg-app_prod_eastus',
|
|
enableRBAC: true,
|
|
nodeCount: 2,
|
|
})
|
|
})
|
|
|
|
test('reports a failed provisioning state rather than hiding it', async () => {
|
|
// The local runtime never actually provisions a cluster: every create
|
|
// settles at Failed. Showing that is the whole point of the column.
|
|
runtimeStub([cluster('broken', 'rg-app', 'Failed')])
|
|
const [resource] = await adapter().list()
|
|
|
|
expect(resource?.status).toBe('Failed')
|
|
})
|
|
|
|
test('summarises the agent pools', async () => {
|
|
runtimeStub()
|
|
const [resource] = await adapter().list()
|
|
|
|
expect(resource?.metadata.agentPools).toEqual([
|
|
{name: 'nodepool1', count: 2, vmSize: 'Standard_D2s_v3', osType: 'Linux', mode: 'System'},
|
|
])
|
|
})
|
|
|
|
test('filters the list by search term', async () => {
|
|
runtimeStub([cluster('prod'), cluster('staging')])
|
|
const a = adapter()
|
|
|
|
await expect(a.list({search: 'prod'})).resolves.toHaveLength(1)
|
|
await expect(a.list({search: 'nope'})).resolves.toHaveLength(0)
|
|
})
|
|
|
|
test('inspects a cluster addressed by resource group and name', async () => {
|
|
const calls = runtimeStub()
|
|
const resource = await adapter().get('rg-app/prod')
|
|
|
|
const call = calls.find((c) => c.url.includes('/managedClusters/prod'))
|
|
expect(call?.url).toContain(`/resourceGroups/rg-app/providers/Microsoft.ContainerService/managedClusters/prod`)
|
|
expect(resource?.id).toBe('rg-app/prod')
|
|
})
|
|
|
|
test('rejects an id that does not name a resource group', async () => {
|
|
await expect(adapter().get('prod')).rejects.toThrow(ValidationError)
|
|
})
|
|
|
|
test('returns null when a cluster does not exist', async () => {
|
|
stubFetch((url) => {
|
|
if (url.endsWith('/subscriptions')) return json({value: [{subscriptionId: SUB}]})
|
|
return json({error: {code: 'ResourceNotFound', message: 'not found'}}, 404)
|
|
})
|
|
await expect(adapter().get('rg-app/nope')).resolves.toBeNull()
|
|
})
|
|
|
|
test('refuses create and delete because this runtime cannot provision a cluster', async () => {
|
|
// Deliberately a runtime-specific reason, not a category-wide one: GCP's
|
|
// GKE adapter does support create and delete, so "k8s is read-only
|
|
// everywhere" is false. What holds here is narrower and verified — the
|
|
// shipped floci-az config runs AKS unmocked with no Docker socket to
|
|
// start k3s with, so a created cluster sits at provisioningState Failed
|
|
// forever. Advertising create would be advertising a dead end.
|
|
const a = adapter()
|
|
|
|
await expect(a.create({values: {name: 'prod'}})).rejects.toThrow(NotSupportedError)
|
|
await expect(a.delete('rg-app/prod')).rejects.toThrow(NotSupportedError)
|
|
})
|
|
|
|
test('sends the accept header the other Azure adapters send', async () => {
|
|
// azureJson and cosmosJson both set it, and some ARM endpoints are
|
|
// Accept-sensitive; diverging here makes error handling inconsistent.
|
|
const calls = runtimeStub()
|
|
await adapter().list()
|
|
|
|
const headers = calls[0]?.init?.headers as Record<string, string> | undefined
|
|
expect(headers?.accept).toBe('application/json')
|
|
})
|
|
|
|
test('treats an empty ARM response as no cluster rather than a parse error', async () => {
|
|
stubFetch((url) => {
|
|
if (url.endsWith('/subscriptions')) return json({value: [{subscriptionId: SUB}]})
|
|
return new Response(null, {status: 204})
|
|
})
|
|
|
|
await expect(adapter().get('rg-app/prod')).resolves.toBeNull()
|
|
})
|
|
|
|
test('advertises only list and inspect', () => {
|
|
expect(adapter().schema().actions).toEqual(['list', 'inspect'])
|
|
})
|
|
})
|