import {NotSupportedError} from '../cloud-spi/errors' import {awsEksSchema} from '../cloud-spi/eksSchema' import type { CloudResource, CloudServiceAdapter, CreateKubernetesFargateProfileInput, CreateKubernetesNodegroupInput, CreateResourceInput, KubernetesFargateProfile, KubernetesNodegroup, ResourceQuery, ServiceSchema, } from '../cloud-spi/types' import {ValidationError} from '../cloud-spi/errors' import {eksService, type EksCluster, type EksFargateProfile, type EksNodegroup} from '../services/eks' type EksServiceShape = Pick< typeof eksService, | 'listClusters' | 'describeCluster' | 'listNodegroups' | 'createNodegroup' | 'deleteNodegroup' | 'listFargateProfiles' | 'createFargateProfile' | 'deleteFargateProfile' > export class AwsEksAdapter implements CloudServiceAdapter { readonly cloud = 'aws' as const readonly service = 'k8s' as const constructor(private readonly eks: EksServiceShape = eksService) {} schema(): ServiceSchema { return awsEksSchema() } async list(query: ResourceQuery = {}): Promise { const clusters = await this.eks.listClusters() return filterBySearch(clusters.map(toResource), query.search) } async get(id: string): Promise { try { return toResource(await this.eks.describeCluster(id)) } catch (error) { if (hasHttpStatus(error, 404)) return null throw error } } async create(_input: CreateResourceInput): Promise { throw new NotSupportedError('EKS cluster creation is not supported from the dynamic Cloud Explorer.') } async delete(_id: string): Promise { throw new NotSupportedError('EKS cluster deletion is not supported from the dynamic Cloud Explorer.') } async listKubernetesNodegroups(clusterId: string): Promise { return (await this.eks.listNodegroups(clusterId)).map(toNodegroup) } async createKubernetesNodegroup( clusterId: string, input: CreateKubernetesNodegroupInput, ): Promise { validateNodegroupInput(input) return toNodegroup(await this.eks.createNodegroup(clusterId, input)) } async deleteKubernetesNodegroup(clusterId: string, nodegroupId: string): Promise { await this.eks.deleteNodegroup(clusterId, nodegroupId) } async listKubernetesFargateProfiles(clusterId: string): Promise { return (await this.eks.listFargateProfiles(clusterId)).map(toFargateProfile) } async createKubernetesFargateProfile( clusterId: string, input: CreateKubernetesFargateProfileInput, ): Promise { validateFargateProfileInput(input) return toFargateProfile(await this.eks.createFargateProfile(clusterId, input)) } async deleteKubernetesFargateProfile(clusterId: string, profileId: string): Promise { await this.eks.deleteFargateProfile(clusterId, profileId) } } function toResource(cluster: EksCluster): CloudResource { return { id: cluster.name, name: cluster.name, cloud: 'aws', service: 'k8s', type: 'cluster', region: null, createdAt: cluster.createdAt ?? null, status: cluster.status ?? null, version: cluster.version ?? null, metadata: { arn: cluster.arn, endpoint: cluster.endpoint, roleArn: cluster.roleArn, platformVersion: cluster.platformVersion, nodegroupCount: cluster.nodegroupCount ?? 0, fargateProfileCount: cluster.fargateProfileCount ?? 0, resourcesVpcConfig: cluster.resourcesVpcConfig, tags: Object.entries(cluster.tags).map(([key, value]) => ({key, value})), }, } } function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] { const normalized = search?.trim().toLowerCase() if (!normalized) return resources return resources.filter((resource) => resource.name.toLowerCase().includes(normalized)) } function hasHttpStatus(error: unknown, status: number): boolean { if (typeof error !== 'object' || error === null) return false const metadata = (error as {$metadata?: {httpStatusCode?: number}}).$metadata return metadata?.httpStatusCode === status } function toNodegroup(nodegroup: EksNodegroup): KubernetesNodegroup { return { id: nodegroup.name, name: nodegroup.name, clusterId: nodegroup.clusterName, arn: nodegroup.arn ?? null, status: nodegroup.status ?? null, version: nodegroup.version ?? null, releaseVersion: nodegroup.releaseVersion ?? null, createdAt: nodegroup.createdAt ?? null, modifiedAt: nodegroup.modifiedAt ?? null, capacityType: nodegroup.capacityType ?? null, instanceTypes: nodegroup.instanceTypes, subnets: nodegroup.subnets, nodeRole: nodegroup.nodeRole ?? null, scalingConfig: nodegroup.scalingConfig ?? null, labels: nodegroup.labels, tags: nodegroup.tags, } } function toFargateProfile(profile: EksFargateProfile): KubernetesFargateProfile { return { id: profile.name, name: profile.name, clusterId: profile.clusterName, arn: profile.arn ?? null, status: profile.status ?? null, createdAt: profile.createdAt ?? null, podExecutionRoleArn: profile.podExecutionRoleArn ?? null, subnets: profile.subnets, selectors: profile.selectors.map((selector) => ({ namespace: selector.namespace ?? null, labels: selector.labels, })), tags: profile.tags, } } function validateNodegroupInput(input: CreateKubernetesNodegroupInput): void { if (typeof input.name !== 'string' || !input.name.trim()) throw new ValidationError('Nodegroup name is required') if (typeof input.nodeRole !== 'string' || !input.nodeRole.trim()) throw new ValidationError('Node role ARN is required') if (!Array.isArray(input.subnets) || input.subnets.length === 0 || input.subnets.some((subnet) => typeof subnet !== 'string' || !subnet.trim())) { throw new ValidationError('At least one subnet is required') } } function validateFargateProfileInput(input: CreateKubernetesFargateProfileInput): void { if (typeof input.name !== 'string' || !input.name.trim()) throw new ValidationError('Fargate profile name is required') if (typeof input.podExecutionRoleArn !== 'string' || !input.podExecutionRoleArn.trim()) throw new ValidationError('Pod execution role ARN is required') if (!Array.isArray(input.selectors) || input.selectors.length === 0 || input.selectors.some((selector) => typeof selector.namespace !== 'string' || !selector.namespace.trim())) { throw new ValidationError('At least one selector namespace is required') } }