first commit
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user