@appliance.sh/api-server 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -44
- package/src/app.controller.spec.ts +10 -20
- package/src/main.ts +22 -4
- package/src/routes/index.ts +7 -0
- package/src/routes/infra/index.ts +24 -0
- package/src/services/ApplianceStack.ts +176 -0
- package/src/{pulumi → services}/pulumi.service.ts +25 -9
- package/test/app.e2e-spec.ts +11 -15
- package/vitest.config.ts +8 -0
- package/vitest.e2e.config.ts +8 -0
- package/nest-cli.json +0 -8
- package/src/app.controller.ts +0 -12
- package/src/app.module.ts +0 -11
- package/src/app.service.ts +0 -8
- package/src/pulumi/ApplianceStack.ts +0 -86
- package/src/pulumi/pulumi.controller.ts +0 -19
- package/src/pulumi/pulumi.module.ts +0 -9
- package/test/jest-e2e.json +0 -9
package/package.json
CHANGED
|
@@ -1,73 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appliance.sh/api-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Eliot Lim",
|
|
6
6
|
"repository": "https://github.com/appliance-sh/appliance.sh",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "
|
|
9
|
+
"build": "tsc",
|
|
10
10
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
11
|
-
"start": "
|
|
12
|
-
"start:dev": "
|
|
13
|
-
"start:debug": "
|
|
14
|
-
"start:prod": "node dist/main",
|
|
11
|
+
"start": "node dist/src/main.js",
|
|
12
|
+
"start:dev": "tsx watch src/main.ts",
|
|
13
|
+
"start:debug": "tsx watch --inspect src/main.ts",
|
|
14
|
+
"start:prod": "node dist/src/main.js",
|
|
15
15
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
16
|
-
"test": "
|
|
17
|
-
"test:watch": "
|
|
18
|
-
"test:cov": "
|
|
19
|
-
"test:
|
|
20
|
-
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:cov": "vitest run --coverage",
|
|
19
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts"
|
|
21
20
|
},
|
|
22
21
|
"dependencies": {
|
|
23
|
-
"@nestjs/common": "^11.0.1",
|
|
24
|
-
"@nestjs/core": "^11.0.1",
|
|
25
|
-
"@nestjs/platform-express": "^11.0.1",
|
|
26
22
|
"@pulumi/aws": "^7.15.0",
|
|
27
23
|
"@pulumi/pulumi": "^3.213.0",
|
|
28
|
-
"
|
|
29
|
-
"rxjs": "^7.8.1"
|
|
24
|
+
"express": "^5.2.1"
|
|
30
25
|
},
|
|
31
26
|
"devDependencies": {
|
|
32
27
|
"@eslint/eslintrc": "^3.2.0",
|
|
33
28
|
"@eslint/js": "^9.18.0",
|
|
34
|
-
"@nestjs/cli": "^11.0.0",
|
|
35
|
-
"@nestjs/schematics": "^11.0.0",
|
|
36
|
-
"@nestjs/testing": "^11.0.1",
|
|
37
29
|
"@types/express": "^5.0.0",
|
|
38
|
-
"@types/jest": "^30.0.0",
|
|
39
30
|
"@types/node": "^22.10.7",
|
|
40
31
|
"@types/supertest": "^6.0.2",
|
|
41
32
|
"eslint": "^9.18.0",
|
|
42
33
|
"eslint-config-prettier": "^10.0.1",
|
|
43
34
|
"eslint-plugin-prettier": "^5.2.2",
|
|
44
35
|
"globals": "^16.0.0",
|
|
45
|
-
"jest": "^30.0.0",
|
|
46
36
|
"prettier": "^3.4.2",
|
|
47
|
-
"source-map-support": "^0.5.21",
|
|
48
37
|
"supertest": "^7.0.0",
|
|
49
|
-
"
|
|
50
|
-
"ts-loader": "^9.5.2",
|
|
51
|
-
"ts-node": "^10.9.2",
|
|
52
|
-
"tsconfig-paths": "^4.2.0",
|
|
38
|
+
"tsx": "^4.19.2",
|
|
53
39
|
"typescript": "^5.7.3",
|
|
54
|
-
"typescript-eslint": "^8.20.0"
|
|
55
|
-
|
|
56
|
-
"jest": {
|
|
57
|
-
"moduleFileExtensions": [
|
|
58
|
-
"js",
|
|
59
|
-
"json",
|
|
60
|
-
"ts"
|
|
61
|
-
],
|
|
62
|
-
"rootDir": "src",
|
|
63
|
-
"testRegex": ".*\\.spec\\.ts$",
|
|
64
|
-
"transform": {
|
|
65
|
-
"^.+\\.(t|j)s$": "ts-jest"
|
|
66
|
-
},
|
|
67
|
-
"collectCoverageFrom": [
|
|
68
|
-
"**/*.(t|j)s"
|
|
69
|
-
],
|
|
70
|
-
"coverageDirectory": "../coverage",
|
|
71
|
-
"testEnvironment": "node"
|
|
40
|
+
"typescript-eslint": "^8.20.0",
|
|
41
|
+
"vitest": "^3.0.4"
|
|
72
42
|
}
|
|
73
43
|
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
describe('
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
providers: [AppService],
|
|
12
|
-
}).compile();
|
|
13
|
-
|
|
14
|
-
appController = app.get<AppController>(AppController);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('root', () => {
|
|
18
|
-
it('should return "Hello World!"', () => {
|
|
19
|
-
expect(appController.getHello()).toBe('Hello World!');
|
|
20
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import { createApp } from './main';
|
|
4
|
+
|
|
5
|
+
describe('Index Route', () => {
|
|
6
|
+
it('should return "Hello World!"', async () => {
|
|
7
|
+
const app = createApp();
|
|
8
|
+
const response = await request(app).get('/');
|
|
9
|
+
expect(response.status).toBe(200);
|
|
10
|
+
expect(response.text).toBe('Hello World!');
|
|
21
11
|
});
|
|
22
12
|
});
|
package/src/main.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { indexRoutes } from './routes';
|
|
3
|
+
import { infraRoutes } from './routes/infra';
|
|
4
|
+
|
|
5
|
+
export function createApp() {
|
|
6
|
+
const app = express();
|
|
7
|
+
|
|
8
|
+
app.use(express.json());
|
|
9
|
+
|
|
10
|
+
// Set up routes
|
|
11
|
+
app.use('/', indexRoutes);
|
|
12
|
+
app.use('/infra', infraRoutes);
|
|
13
|
+
|
|
14
|
+
return app;
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
async function bootstrap() {
|
|
5
|
-
const app =
|
|
6
|
-
|
|
18
|
+
const app = createApp();
|
|
19
|
+
const port = process.env.PORT ?? 3000;
|
|
20
|
+
|
|
21
|
+
app.listen(port, () => {
|
|
22
|
+
console.log(`Server is running on http://localhost:${port}`);
|
|
23
|
+
});
|
|
7
24
|
}
|
|
25
|
+
|
|
8
26
|
bootstrap();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { pulumiService } from '../../services/pulumi.service';
|
|
3
|
+
|
|
4
|
+
export const infraRoutes = Router();
|
|
5
|
+
|
|
6
|
+
infraRoutes.post('/deploy', async (_req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
const result = await pulumiService.deploy();
|
|
9
|
+
res.json(result);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Deploy error:', error);
|
|
12
|
+
res.status(500).json({ error: 'Deploy failed', message: String(error) });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
infraRoutes.post('/destroy', async (_req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const result = await pulumiService.destroy();
|
|
19
|
+
res.json(result);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Destroy error:', error);
|
|
22
|
+
res.status(500).json({ error: 'Destroy failed', message: String(error) });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as pulumi from '@pulumi/pulumi';
|
|
2
|
+
import * as aws from '@pulumi/aws';
|
|
3
|
+
import * as awsNative from '@pulumi/aws-native';
|
|
4
|
+
import type { ApplianceBaseConfig } from '@appliance.sh/sdk';
|
|
5
|
+
|
|
6
|
+
export interface ApplianceStackArgs {
|
|
7
|
+
tags?: Record<string, string>;
|
|
8
|
+
config: ApplianceBaseConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ApplianceStackOpts extends pulumi.ComponentResourceOptions {
|
|
12
|
+
globalProvider: aws.Provider;
|
|
13
|
+
provider: aws.Provider;
|
|
14
|
+
nativeProvider: awsNative.Provider;
|
|
15
|
+
nativeGlobalProvider: awsNative.Provider;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ApplianceStack extends pulumi.ComponentResource {
|
|
19
|
+
lambdaRole: aws.iam.Role;
|
|
20
|
+
lambdaRolePolicy: aws.iam.Policy;
|
|
21
|
+
lambda: aws.lambda.Function;
|
|
22
|
+
lambdaUrl: aws.lambda.FunctionUrl;
|
|
23
|
+
dnsRecord: pulumi.Output<string>;
|
|
24
|
+
|
|
25
|
+
constructor(name: string, args: ApplianceStackArgs, opts: ApplianceStackOpts) {
|
|
26
|
+
super('appliance:aws:ApplianceStack', name, args, opts);
|
|
27
|
+
|
|
28
|
+
const defaultOpts = { parent: this, provider: opts.provider };
|
|
29
|
+
const defaultNativeOpts = { parent: this, provider: opts.nativeProvider };
|
|
30
|
+
const defaultTags = { stack: name, managed: 'appliance', ...args.tags };
|
|
31
|
+
|
|
32
|
+
this.lambdaRole = new aws.iam.Role(`${name}-role`, {
|
|
33
|
+
path: `/appliance/${name}/`,
|
|
34
|
+
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: 'lambda.amazonaws.com' }),
|
|
35
|
+
tags: defaultTags,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.lambdaRolePolicy = new aws.iam.Policy(`${name}-policy`, {
|
|
39
|
+
path: `/appliance/${name}/`,
|
|
40
|
+
policy: {
|
|
41
|
+
Version: '2012-10-17',
|
|
42
|
+
Statement: [{ Effect: 'Allow', Action: 'logs:CreateLogGroup', Resource: '*' }],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
new aws.iam.RolePolicyAttachment(`${name}-role-policy-attachment`, {
|
|
47
|
+
role: this.lambdaRole.name,
|
|
48
|
+
policyArn: this.lambdaRolePolicy.arn,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.lambda = new aws.lambda.CallbackFunction(
|
|
52
|
+
`${name}-handler`,
|
|
53
|
+
{
|
|
54
|
+
runtime: 'nodejs22.x',
|
|
55
|
+
callback: async () => {
|
|
56
|
+
return { statusCode: 200, body: JSON.stringify({ message: 'Hello world!' }) };
|
|
57
|
+
},
|
|
58
|
+
tags: defaultTags,
|
|
59
|
+
},
|
|
60
|
+
defaultOpts
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// lambda url
|
|
64
|
+
this.lambdaUrl = new aws.lambda.FunctionUrl(
|
|
65
|
+
`${name}-url`,
|
|
66
|
+
{
|
|
67
|
+
functionName: this.lambda.name,
|
|
68
|
+
authorizationType: args.config.aws.cloudfrontDistributionId ? 'AWS_IAM' : 'NONE',
|
|
69
|
+
},
|
|
70
|
+
defaultOpts
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
this.dnsRecord = pulumi.interpolate`${name}.${args.config.domainName ?? ''}`;
|
|
74
|
+
|
|
75
|
+
if (args.config.aws.cloudfrontDistributionId) {
|
|
76
|
+
new aws.lambda.Permission(
|
|
77
|
+
`${name}-url-invoke-url-permission`,
|
|
78
|
+
{
|
|
79
|
+
function: this.lambda.name,
|
|
80
|
+
action: 'lambda:InvokeFunctionUrl',
|
|
81
|
+
principal: 'cloudfront.amazonaws.com',
|
|
82
|
+
functionUrlAuthType: 'AWS_IAM',
|
|
83
|
+
sourceArn: pulumi.interpolate`arn:aws:cloudfront::${
|
|
84
|
+
aws.getCallerIdentityOutput({}, { provider: opts.provider }).accountId
|
|
85
|
+
}:distribution/${args.config.aws.cloudfrontDistributionId}`,
|
|
86
|
+
statementId: 'FunctionURLAllowCloudFrontAccess',
|
|
87
|
+
},
|
|
88
|
+
defaultOpts
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Grant the edge router role permission to invoke the Lambda Function URL
|
|
92
|
+
// The edge router role is the execution role of the Lambda@Edge function that signs requests
|
|
93
|
+
if (args.config.aws.edgeRouterRoleArn) {
|
|
94
|
+
new aws.lambda.Permission(
|
|
95
|
+
`${name}-invoke-url-edge-router-permission`,
|
|
96
|
+
{
|
|
97
|
+
function: this.lambda.name,
|
|
98
|
+
action: 'lambda:InvokeFunctionUrl',
|
|
99
|
+
principal: args.config.aws.edgeRouterRoleArn,
|
|
100
|
+
functionUrlAuthType: 'AWS_IAM',
|
|
101
|
+
statementId: 'FunctionURLAllowEdgeRouterRoleAccess',
|
|
102
|
+
},
|
|
103
|
+
defaultOpts
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
new awsNative.lambda.Permission(
|
|
107
|
+
`${name}-invoke-edge-router-permission`,
|
|
108
|
+
{
|
|
109
|
+
action: 'lambda:InvokeFunction',
|
|
110
|
+
principal: args.config.aws.edgeRouterRoleArn,
|
|
111
|
+
functionName: this.lambda.name,
|
|
112
|
+
invokedViaFunctionUrl: true,
|
|
113
|
+
},
|
|
114
|
+
defaultNativeOpts
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
new aws.lambda.Permission(
|
|
119
|
+
`${name}-url-invoke-url-permission`,
|
|
120
|
+
{
|
|
121
|
+
function: this.lambda.name,
|
|
122
|
+
action: 'lambda:InvokeFunctionUrl',
|
|
123
|
+
principal: '*',
|
|
124
|
+
functionUrlAuthType: 'NONE',
|
|
125
|
+
statementId: 'FunctionURLAllowPublicAccess',
|
|
126
|
+
},
|
|
127
|
+
defaultOpts
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (args.config.aws.cloudfrontDistributionId && args.config.aws.cloudfrontDistributionDomainName) {
|
|
132
|
+
new awsNative.lambda.Permission(
|
|
133
|
+
`${name}-url-invoke-lambda-native-permission`,
|
|
134
|
+
{
|
|
135
|
+
action: 'lambda:InvokeFunction',
|
|
136
|
+
principal: 'cloudfront.amazonaws.com',
|
|
137
|
+
sourceArn: pulumi.interpolate`arn:aws:cloudfront::${
|
|
138
|
+
aws.getCallerIdentityOutput({}, { provider: opts.provider }).accountId
|
|
139
|
+
}:distribution/${args.config.aws.cloudfrontDistributionId}`,
|
|
140
|
+
functionName: this.lambda.name,
|
|
141
|
+
invokedViaFunctionUrl: true,
|
|
142
|
+
},
|
|
143
|
+
defaultNativeOpts
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
new aws.route53.Record(
|
|
147
|
+
`${name}-cname-record`,
|
|
148
|
+
{
|
|
149
|
+
zoneId: args.config.aws.zoneId,
|
|
150
|
+
name: pulumi.interpolate`${name}.${args.config.domainName ?? ''}`,
|
|
151
|
+
type: 'CNAME',
|
|
152
|
+
ttl: 60,
|
|
153
|
+
records: [args.config.aws.cloudfrontDistributionDomainName],
|
|
154
|
+
},
|
|
155
|
+
{ parent: this, provider: opts.globalProvider }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
new aws.route53.Record(
|
|
159
|
+
`${name}-txt-record`,
|
|
160
|
+
{
|
|
161
|
+
zoneId: args.config.aws.zoneId,
|
|
162
|
+
name: pulumi.interpolate`origin.${name}.${args.config.domainName ?? ''}`,
|
|
163
|
+
type: 'TXT',
|
|
164
|
+
ttl: 60,
|
|
165
|
+
records: [this.lambdaUrl.functionUrl],
|
|
166
|
+
},
|
|
167
|
+
{ parent: this, provider: opts.globalProvider }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.registerOutputs({
|
|
172
|
+
lambda: this.lambda,
|
|
173
|
+
lambdaUrl: this.lambdaUrl,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Injectable, Logger } from '@nestjs/common';
|
|
2
1
|
import * as auto from '@pulumi/pulumi/automation';
|
|
3
2
|
import * as aws from '@pulumi/aws';
|
|
3
|
+
import * as awsNative from '@pulumi/aws-native';
|
|
4
4
|
import { ApplianceStack } from './ApplianceStack';
|
|
5
5
|
import { applianceBaseConfig } from '@appliance.sh/sdk';
|
|
6
6
|
|
|
@@ -14,34 +14,47 @@ export interface PulumiResult {
|
|
|
14
14
|
stackName: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
export class PulumiService {
|
|
19
|
-
private readonly logger = new Logger(PulumiService.name);
|
|
20
|
-
private readonly region = process.env.AWS_REGION || 'us-east-1';
|
|
17
|
+
class PulumiService {
|
|
21
18
|
private readonly projectName = 'appliance-api-managed-proj';
|
|
22
19
|
|
|
23
20
|
private readonly baseConfig = process.env.APPLIANCE_BASE_CONFIG
|
|
24
21
|
? applianceBaseConfig.parse(JSON.parse(process.env.APPLIANCE_BASE_CONFIG))
|
|
25
22
|
: undefined;
|
|
23
|
+
private readonly region = this.baseConfig?.aws.region || 'us-east-1';
|
|
26
24
|
|
|
27
25
|
private inlineProgram() {
|
|
28
26
|
return async () => {
|
|
29
27
|
const name = 'appliance';
|
|
28
|
+
|
|
29
|
+
if (!this.baseConfig) {
|
|
30
|
+
throw new Error('Missing base config');
|
|
31
|
+
}
|
|
32
|
+
|
|
30
33
|
const regionalProvider = new aws.Provider(`${name}-regional`, {
|
|
31
|
-
region: this.baseConfig?.region ?? 'ap-southeast-1',
|
|
34
|
+
region: (this.baseConfig?.aws.region as aws.Region) ?? 'ap-southeast-1',
|
|
32
35
|
});
|
|
33
36
|
const globalProvider = new aws.Provider(`${name}-global`, {
|
|
34
37
|
region: 'us-east-1',
|
|
35
38
|
});
|
|
39
|
+
const nativeRegionalProvider = new awsNative.Provider(`${name}-native-regional`, {
|
|
40
|
+
region: (this.baseConfig?.aws.region as awsNative.Region) ?? 'ap-southeast-1',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const nativeGlobalProvider = new awsNative.Provider(`${name}-native-global`, {
|
|
44
|
+
region: 'us-east-1',
|
|
45
|
+
});
|
|
36
46
|
|
|
37
47
|
const applianceStack = new ApplianceStack(
|
|
38
48
|
`${name}-stack`,
|
|
39
49
|
{
|
|
40
50
|
tags: { project: name },
|
|
51
|
+
config: this.baseConfig,
|
|
41
52
|
},
|
|
42
53
|
{
|
|
43
54
|
globalProvider,
|
|
44
55
|
provider: regionalProvider,
|
|
56
|
+
nativeProvider: nativeRegionalProvider,
|
|
57
|
+
nativeGlobalProvider: nativeGlobalProvider,
|
|
45
58
|
}
|
|
46
59
|
);
|
|
47
60
|
|
|
@@ -67,7 +80,7 @@ export class PulumiService {
|
|
|
67
80
|
{ projectName: this.projectName, stackName, program },
|
|
68
81
|
{ envVars }
|
|
69
82
|
);
|
|
70
|
-
await stack.setConfig('aws:region', { value: this.region });
|
|
83
|
+
await stack.setConfig('aws:region', { value: this.baseConfig.aws.region });
|
|
71
84
|
return stack;
|
|
72
85
|
}
|
|
73
86
|
|
|
@@ -92,7 +105,7 @@ export class PulumiService {
|
|
|
92
105
|
|
|
93
106
|
async deploy(stackName = 'appliance-api-managed'): Promise<PulumiResult> {
|
|
94
107
|
const stack = await this.getOrCreateStack(stackName);
|
|
95
|
-
const result = await stack.up({ onOutput: (m) =>
|
|
108
|
+
const result = await stack.up({ onOutput: (m) => console.log(m) });
|
|
96
109
|
const changes = result.summary.resourceChanges || {};
|
|
97
110
|
const totalChanges = Object.entries(changes)
|
|
98
111
|
.filter(([k]) => k !== 'same')
|
|
@@ -110,7 +123,7 @@ export class PulumiService {
|
|
|
110
123
|
async destroy(stackName = 'appliance-api-managed'): Promise<PulumiResult> {
|
|
111
124
|
try {
|
|
112
125
|
const stack = await this.selectExistingStack(stackName);
|
|
113
|
-
await stack.destroy({ onOutput: (m) =>
|
|
126
|
+
await stack.destroy({ onOutput: (m) => console.log(m) });
|
|
114
127
|
return { action: 'destroy', ok: true, idempotentNoop: false, message: 'Stack resources deleted', stackName };
|
|
115
128
|
} catch (e) {
|
|
116
129
|
if (!(e instanceof Error)) throw e;
|
|
@@ -128,3 +141,6 @@ export class PulumiService {
|
|
|
128
141
|
}
|
|
129
142
|
}
|
|
130
143
|
}
|
|
144
|
+
|
|
145
|
+
// Export a singleton instance
|
|
146
|
+
export const pulumiService = new PulumiService();
|
package/test/app.e2e-spec.ts
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { INestApplication } from '@nestjs/common';
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
3
2
|
import request from 'supertest';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { createApp } from '../src/main';
|
|
4
|
+
import type { Express } from 'express';
|
|
6
5
|
|
|
7
|
-
describe('
|
|
8
|
-
let app:
|
|
6
|
+
describe('App (e2e)', () => {
|
|
7
|
+
let app: Express;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
imports: [AppModule],
|
|
13
|
-
}).compile();
|
|
14
|
-
|
|
15
|
-
app = moduleFixture.createNestApplication();
|
|
16
|
-
await app.init();
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
app = createApp();
|
|
17
11
|
});
|
|
18
12
|
|
|
19
|
-
it('/ (GET)', () => {
|
|
20
|
-
|
|
13
|
+
it('/ (GET)', async () => {
|
|
14
|
+
const response = await request(app).get('/');
|
|
15
|
+
expect(response.status).toBe(200);
|
|
16
|
+
expect(response.text).toBe('Hello World!');
|
|
21
17
|
});
|
|
22
18
|
});
|
package/vitest.config.ts
ADDED
package/nest-cli.json
DELETED
package/src/app.controller.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Controller, Get } from '@nestjs/common';
|
|
2
|
-
import { AppService } from './app.service';
|
|
3
|
-
|
|
4
|
-
@Controller()
|
|
5
|
-
export class AppController {
|
|
6
|
-
constructor(private readonly appService: AppService) {}
|
|
7
|
-
|
|
8
|
-
@Get()
|
|
9
|
-
getHello(): string {
|
|
10
|
-
return this.appService.getHello();
|
|
11
|
-
}
|
|
12
|
-
}
|
package/src/app.module.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { AppController } from './app.controller';
|
|
3
|
-
import { AppService } from './app.service';
|
|
4
|
-
import { PulumiModule } from './pulumi/pulumi.module';
|
|
5
|
-
|
|
6
|
-
@Module({
|
|
7
|
-
imports: [PulumiModule],
|
|
8
|
-
controllers: [AppController],
|
|
9
|
-
providers: [AppService],
|
|
10
|
-
})
|
|
11
|
-
export class AppModule {}
|
package/src/app.service.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import * as pulumi from '@pulumi/pulumi';
|
|
2
|
-
import * as aws from '@pulumi/aws';
|
|
3
|
-
|
|
4
|
-
export interface ApplianceStackArgs {
|
|
5
|
-
tags?: Record<string, string>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ApplianceStackOpts extends pulumi.ComponentResourceOptions {
|
|
9
|
-
globalProvider: aws.Provider;
|
|
10
|
-
provider: aws.Provider;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class ApplianceStack extends pulumi.ComponentResource {
|
|
14
|
-
lambdaRole: aws.iam.Role;
|
|
15
|
-
lambdaRolePolicy: aws.iam.Policy;
|
|
16
|
-
lambda: aws.lambda.Function;
|
|
17
|
-
lambdaUrl: aws.lambda.FunctionUrl;
|
|
18
|
-
|
|
19
|
-
constructor(name: string, args: ApplianceStackArgs, opts: ApplianceStackOpts) {
|
|
20
|
-
super('appliance:aws:ApplianceStack', name, args, opts);
|
|
21
|
-
|
|
22
|
-
const defaultOpts = { parent: this, provider: opts.provider };
|
|
23
|
-
const defaultTags = { stack: name, managed: 'appliance', ...args.tags };
|
|
24
|
-
|
|
25
|
-
this.lambdaRole = new aws.iam.Role(`${name}-role`, {
|
|
26
|
-
path: `/appliance/${name}/`,
|
|
27
|
-
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: 'lambda.amazonaws.com' }),
|
|
28
|
-
tags: defaultTags,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
this.lambdaRolePolicy = new aws.iam.Policy(`${name}-policy`, {
|
|
32
|
-
path: `/appliance/${name}/`,
|
|
33
|
-
policy: {
|
|
34
|
-
Version: '2012-10-17',
|
|
35
|
-
Statement: [{ Effect: 'Allow', Action: 'logs:CreateLogGroup', Resource: '*' }],
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
new aws.iam.RolePolicyAttachment(`${name}-role-policy-attachment`, {
|
|
40
|
-
role: this.lambdaRole.name,
|
|
41
|
-
policyArn: this.lambdaRolePolicy.arn,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
this.lambda = new aws.lambda.CallbackFunction(
|
|
45
|
-
`${name}-handler`,
|
|
46
|
-
{
|
|
47
|
-
runtime: 'nodejs22.x',
|
|
48
|
-
callback: async () => {
|
|
49
|
-
return { statusCode: 200, body: JSON.stringify({ message: 'Hello world!' }) };
|
|
50
|
-
},
|
|
51
|
-
tags: defaultTags,
|
|
52
|
-
},
|
|
53
|
-
defaultOpts
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
// lambda url
|
|
57
|
-
this.lambdaUrl = new aws.lambda.FunctionUrl(
|
|
58
|
-
`${name}-url`,
|
|
59
|
-
{
|
|
60
|
-
functionName: this.lambda.name,
|
|
61
|
-
authorizationType: 'NONE',
|
|
62
|
-
},
|
|
63
|
-
defaultOpts
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
new aws.lambda.Permission(`${name}-url-invoke-url-permission`, {
|
|
67
|
-
function: this.lambda.name,
|
|
68
|
-
action: 'lambda:InvokeFunctionUrl',
|
|
69
|
-
principal: '*',
|
|
70
|
-
functionUrlAuthType: 'NONE',
|
|
71
|
-
statementId: 'FunctionURLAllowPublicAccess',
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
new aws.lambda.Permission(`${name}-url-invoke-lambda-permission`, {
|
|
75
|
-
function: this.lambda.name,
|
|
76
|
-
action: 'lambda:InvokeFunction',
|
|
77
|
-
principal: '*',
|
|
78
|
-
statementId: 'FunctionURLAllowInvokeAction',
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
this.registerOutputs({
|
|
82
|
-
lambda: this.lambda,
|
|
83
|
-
lambdaUrl: this.lambdaUrl,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Controller, HttpCode, Post } from '@nestjs/common';
|
|
2
|
-
import { PulumiService } from './pulumi.service';
|
|
3
|
-
|
|
4
|
-
@Controller('infra')
|
|
5
|
-
export class PulumiController {
|
|
6
|
-
constructor(private readonly pulumi: PulumiService) {}
|
|
7
|
-
|
|
8
|
-
@Post('deploy')
|
|
9
|
-
@HttpCode(200)
|
|
10
|
-
async deploy() {
|
|
11
|
-
return await this.pulumi.deploy();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
@Post('destroy')
|
|
15
|
-
@HttpCode(200)
|
|
16
|
-
async destroy() {
|
|
17
|
-
return await this.pulumi.destroy();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { PulumiService } from './pulumi.service';
|
|
3
|
-
import { PulumiController } from './pulumi.controller';
|
|
4
|
-
|
|
5
|
-
@Module({
|
|
6
|
-
providers: [PulumiService],
|
|
7
|
-
controllers: [PulumiController],
|
|
8
|
-
})
|
|
9
|
-
export class PulumiModule {}
|