Skip to content

Commit 00f520b

Browse files
committed
feat(api): added kubernetes api
1 parent 83e9606 commit 00f520b

File tree

11 files changed

+321
-1
lines changed

11 files changed

+321
-1
lines changed

apps/api/src/app/app.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { UserModule } from './user';
88
import { AppController } from './app.controller';
99
import { NotificationsModule } from './notifications';
1010
import { PushModule } from './push';
11+
import { ExternalModule } from './external';
1112

1213
@Module({
1314
imports: [
@@ -22,14 +23,16 @@ import { PushModule } from './push';
2223
{ path: '/notifications', module: NotificationsModule },
2324
],
2425
},
26+
{ path: '/external', module: ExternalModule },
2527
]),
2628
CoreModule,
2729
AuthModule,
2830
UserModule,
2931
// AccountModule,
32+
// ChatModule,
33+
ExternalModule,
3034
NotificationsModule,
3135
PushModule,
32-
// ChatModule,
3336
],
3437
controllers: [AppController],
3538
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { HttpModule, Module } from '@nestjs/common';
2+
import { SharedModule } from '../shared';
3+
import { KubernetesService } from './kubernetes/kubernetes.service';
4+
import { KubernetesController } from './kubernetes/kubernetes.controller';
5+
6+
@Module({
7+
imports: [SharedModule, HttpModule.register({ timeout: 5000 })],
8+
providers: [KubernetesService],
9+
controllers: [KubernetesController],
10+
})
11+
export class ExternalModule {}

apps/api/src/app/external/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './external.module';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { IsEnum } from 'class-validator';
2+
import { ApiModelProperty } from '@nestjs/swagger';
3+
4+
export enum EnvironmentType {
5+
Prod,
6+
NonProd,
7+
}
8+
export enum ZoneType {
9+
DMZ,
10+
Core,
11+
}
12+
13+
export enum DCType {
14+
CLUSTER1,
15+
CLUSTER2,
16+
CLUSTER3,
17+
CLUSTER4,
18+
}
19+
20+
export class Labels {
21+
@ApiModelProperty({ enum: ['Prod', 'NonProd'] })
22+
@IsEnum(EnvironmentType)
23+
env: EnvironmentType;
24+
25+
@ApiModelProperty({ required: false, enum: ['Core', 'DMZ'] })
26+
@IsEnum(ZoneType)
27+
zone?: ZoneType;
28+
29+
@ApiModelProperty({ required: false, enum: ['CLUSTER1', 'CLUSTER2', 'CLUSTER3', 'CLUSTER4'] })
30+
@IsEnum(DCType)
31+
dc?: DCType;
32+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { IsArray, IsAscii, IsOptional, IsString, Length, Matches, ValidateNested } from 'class-validator';
2+
import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger';
3+
import { Labels } from './labels.dto';
4+
import { Type } from 'class-transformer';
5+
6+
export class SearchProjectDto {
7+
@ApiModelProperty({ type: [String] })
8+
@IsArray()
9+
@IsOptional()
10+
readonly groups?: string[];
11+
12+
@ApiModelProperty({ type: String })
13+
@IsString()
14+
@IsOptional()
15+
readonly userId?: string;
16+
17+
@ApiModelPropertyOptional({ type: String })
18+
@IsAscii()
19+
@IsOptional()
20+
@Length(5, 100)
21+
@Matches(/^[a-z\d-]+$/)
22+
readonly namespace?: string;
23+
24+
@ApiModelPropertyOptional({ type: String, minLength: 5, maxLength: 100 })
25+
@IsAscii()
26+
@IsOptional()
27+
@Length(5, 100)
28+
@Matches(/^[a-z\d-]+$/)
29+
readonly serviceAccountName: string;
30+
31+
@ApiModelPropertyOptional({ type: Labels })
32+
@IsOptional()
33+
@ValidateNested()
34+
@Type(() => Labels)
35+
readonly labels?: Labels;
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { KubernetesController } from './kubernetes.controller';
3+
4+
describe('Kubernetes Controller', () => {
5+
let module: TestingModule;
6+
beforeAll(async () => {
7+
module = await Test.createTestingModule({
8+
controllers: [KubernetesController],
9+
}).compile();
10+
});
11+
it('should be defined', () => {
12+
const controller: KubernetesController = module.get<KubernetesController>(KubernetesController);
13+
expect(controller).toBeDefined();
14+
});
15+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Controller, Get, HttpStatus, Logger, Param } from '@nestjs/common';
2+
import { ApiOAuth2Auth, ApiOperation, ApiResponse, ApiUseTags } from '@nestjs/swagger';
3+
import { KubernetesService } from './kubernetes.service';
4+
5+
@ApiOAuth2Auth(['read'])
6+
@ApiUseTags('External', 'Kubernetes')
7+
@ApiResponse({ status: HttpStatus.OK, description: 'Found' })
8+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Not Found' })
9+
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
10+
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden' })
11+
@Controller('kubernetes')
12+
export class KubernetesController {
13+
public readonly logger = new Logger(KubernetesController.name);
14+
15+
constructor(private readonly kubernetesService: KubernetesService) {}
16+
17+
@ApiOperation({ title: 'Get all namespaces im a cluster' })
18+
@Get(':cluster')
19+
getAll(@Param('cluster') cluster: string): Promise<any> {
20+
return this.kubernetesService.listNamespaces(cluster);
21+
}
22+
23+
@ApiOperation({ title: 'Find one namespace in a cluster by namespace' })
24+
@Get(':cluster/:namespace')
25+
findOne(@Param('cluster') cluster: string, @Param('namespace') namespace: string): Promise<any> {
26+
return this.kubernetesService.getNamespace(cluster, namespace);
27+
}
28+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { KubernetesService } from './kubernetes.service';
3+
4+
describe('KubernetesService', () => {
5+
let service: KubernetesService;
6+
beforeAll(async () => {
7+
const module: TestingModule = await Test.createTestingModule({
8+
providers: [KubernetesService],
9+
}).compile();
10+
service = module.get<KubernetesService>(KubernetesService);
11+
});
12+
it('should be defined', () => {
13+
expect(service).toBeDefined();
14+
});
15+
});
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
BadRequestException,
3+
ConflictException,
4+
HttpException,
5+
HttpStatus,
6+
Injectable,
7+
Logger,
8+
NotFoundException,
9+
OnModuleInit,
10+
UnauthorizedException,
11+
} from '@nestjs/common';
12+
import * as Api from 'kubernetes-client';
13+
import { ConfigService } from '../../config';
14+
import { environment as env } from '@env-api/environment';
15+
16+
const Client = Api.Client1_10;
17+
const config = Api.config;
18+
19+
@Injectable()
20+
export class KubernetesService implements OnModuleInit {
21+
private readonly logger = new Logger(KubernetesService.name);
22+
23+
private readonly clients = new Map(
24+
Object.entries(env.kubernetes).map<[string, Api.Api]>(([key, value]) => [
25+
key,
26+
new Client({
27+
config: {
28+
url: value.baseUrl,
29+
auth: {
30+
bearer: value.token,
31+
},
32+
insecureSkipTlsVerify: true,
33+
version: 'v1',
34+
promises: true,
35+
},
36+
version: value.version || '1.10',
37+
}),
38+
]),
39+
);
40+
41+
constructor(private readonly appConfig: ConfigService) {}
42+
43+
async onModuleInit() {
44+
// @ts-ignore
45+
// for (const [key, client] of this.clients.entries()) {
46+
// try {
47+
// await client.loadSpec();
48+
// } catch (err) {
49+
// console.error(`Unable to connect to ${key}`, err);
50+
// }
51+
// }
52+
}
53+
54+
public async listNamespaces(cluster: string) {
55+
try {
56+
const namespaces = await this.clients.get(cluster).api.v1.namespaces.get();
57+
return namespaces.body.items;
58+
} catch (error) {
59+
KubernetesService.handleError(error);
60+
}
61+
}
62+
63+
public async myNamespaces(cluster: string, token: string) {
64+
try {
65+
// this.client.get(cluster).setToken(token)
66+
const namespaces = await this.clients.get(cluster).api.v1.namespaces.get();
67+
return namespaces.items;
68+
} catch (error) {
69+
KubernetesService.handleError(error);
70+
}
71+
}
72+
73+
public async getNamespace(cluster: string, namespace: string) {
74+
try {
75+
const namespace1 = await this.clients
76+
.get(cluster)
77+
.api.v1.namespaces(namespace)
78+
.get();
79+
return namespace1.body;
80+
} catch (error) {
81+
KubernetesService.handleError(error);
82+
}
83+
}
84+
85+
public async myServiceAccounts(cluster: string, namespace: string) {
86+
try {
87+
const namespaces = await this.clients
88+
.get(cluster)
89+
.api.v1.namespaces(namespace)
90+
.serviceaccounts.get();
91+
return namespaces.body.items;
92+
} catch (error) {
93+
KubernetesService.handleError(error);
94+
}
95+
}
96+
97+
public async hasNamespace(cluster: string, namespace: string) {
98+
try {
99+
const foundNamespace = await this.clients
100+
.get(cluster)
101+
.api.v1.namespaces(namespace)
102+
.get();
103+
return !!foundNamespace;
104+
} catch (error) {
105+
if (error.code === 404) return false;
106+
KubernetesService.handleError(error);
107+
}
108+
}
109+
110+
static handleError(error: Error & { code?: number; statusCode?: number }) {
111+
const message = error.message || 'unknown error';
112+
const statusCode = error.statusCode || error.code || HttpStatus.I_AM_A_TEAPOT;
113+
console.log(message, statusCode);
114+
switch (statusCode) {
115+
case HttpStatus.CONFLICT:
116+
throw new ConflictException(error.message);
117+
case HttpStatus.UNAUTHORIZED:
118+
throw new UnauthorizedException(error.message);
119+
case HttpStatus.NOT_FOUND:
120+
throw new NotFoundException(error.message);
121+
case HttpStatus.BAD_REQUEST:
122+
throw new BadRequestException(error.message);
123+
default:
124+
throw new HttpException(message, statusCode);
125+
}
126+
}
127+
}

apps/api/src/environments/environment.prod.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,30 @@ export const environment = {
5252
'BAJq-yHlSNjUqKW9iMY0hG96X9WdVwetUFDa5rQIGRPqOHKAL_fkKUe_gUTAKnn9IPAltqmlNO2OkJrjdQ_MXNg',
5353
privateKey: process.env.VAPID_PRIVATE_KEY || 'cwh2CYK5h_B_Gobnv8Ym9x61B3qFE2nTeb9BeiZbtMI',
5454
},
55+
kubernetes: {
56+
CLUSTER1: {
57+
baseUrl: 'https://k8s-prod-ctc-aci.optum.com:16443',
58+
version: '1.10',
59+
/* tslint:disable-next-line:max-line-length */
60+
token: process.env.CLUSTER1_SERVICE_ACCOUNT_TOKEN || 'AAAAAAAAAAAA',
61+
},
62+
CLUSTER2: {
63+
baseUrl: 'https://k8s-prod-elr-aci.optum.com:16443',
64+
version: '1.10',
65+
/* tslint:disable-next-line:max-line-length */
66+
token: process.env.CLUSTER2_SERVICE_ACCOUNT_TOKEN || 'BBBBBBBBBBBB',
67+
},
68+
CLUSTER3: {
69+
baseUrl: 'https://k8s-prod-ptc-aci.optum.com:16443',
70+
version: '1.10',
71+
/* tslint:disable-next-line:max-line-length */
72+
token: process.env.CLUSTER3_SERVICE_ACCOUNT_TOKEN || 'CCCCCCCCCCCCC',
73+
},
74+
CLUSTER4: {
75+
baseUrl: 'https://10.176.22.126:6443',
76+
version: '1.10',
77+
/* tslint:disable-next-line:max-line-length */
78+
token: process.env.CLUSTER4_SERVICE_ACCOUNT_TOKEN || 'DDDDDDDDDDDDD',
79+
},
80+
},
5581
};

0 commit comments

Comments
 (0)