Skip to content

Commit 7b3461e

Browse files
authored
Merge pull request #216 from nitrictech/feature/gcp-api-fixes
Gcp API deployment fixes
2 parents 381b56c + 0cfb8ce commit 7b3461e

File tree

2 files changed

+196
-15
lines changed

2 files changed

+196
-15
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2021, Nitric Technologies Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import * as pulumi from '@pulumi/pulumi';
16+
import { NitricComputeCloudRun, NitricApiGcpApiGateway } from '.';
17+
18+
const outputToPromise = async <T>(output: pulumi.Output<T> | undefined): Promise<T | undefined> => {
19+
if (!output) {
20+
return undefined;
21+
}
22+
23+
return await new Promise<T>((res) => {
24+
output.apply((t) => {
25+
res(t);
26+
});
27+
});
28+
};
29+
30+
describe('NitricApiGcpApiGateway', () => {
31+
// Setup pulumi mocks
32+
beforeAll(() => {
33+
pulumi.runtime.setMocks({
34+
newResource: function ({ name, type, inputs }): { id: string; state: any } {
35+
switch (type) {
36+
case 'gcp:apigateway/api:Api':
37+
return {
38+
id: 'mock-api-id',
39+
state: {
40+
...inputs,
41+
// outputs...
42+
name: inputs.name || name + '-sg',
43+
apiId: 'mock-api-id',
44+
},
45+
};
46+
case 'gcp:apigateway/apiConfig:ApiConfig':
47+
return {
48+
id: 'mock-config-id',
49+
state: {
50+
...inputs,
51+
// outputs ...
52+
name: inputs.name || name + '-sg',
53+
},
54+
};
55+
case 'gcp:apigateway/gateway:Gateway':
56+
return {
57+
id: 'mock-gateway-id',
58+
state: {
59+
...inputs,
60+
// outputs ...
61+
name: inputs.name || name + '-sg',
62+
defaultHostName: 'example.com',
63+
},
64+
};
65+
case 'gcp:serviceAccount/account:Account':
66+
return {
67+
id: 'mock-account-id',
68+
state: {
69+
...inputs,
70+
// outputs ...
71+
name: inputs.name || name + '-sg',
72+
73+
},
74+
};
75+
default:
76+
return {
77+
id: inputs.name + '_id',
78+
state: {
79+
...inputs,
80+
},
81+
};
82+
}
83+
},
84+
call: function ({ inputs }) {
85+
return inputs;
86+
},
87+
});
88+
});
89+
90+
describe('When creating a new GcpApiGateway resource', () => {
91+
let api: NitricApiGcpApiGateway | null = null;
92+
beforeAll(() => {
93+
// Create the new Api Gateway resource
94+
api = new NitricApiGcpApiGateway('my-gateway', {
95+
api: {
96+
swagger: '2.0',
97+
info: {
98+
title: 'test-api',
99+
version: '2.0',
100+
},
101+
paths: {
102+
'/example/': {
103+
get: {
104+
operationId: 'getExample',
105+
'x-nitric-target': {
106+
name: 'test-service',
107+
type: 'function',
108+
},
109+
description: 'Retrieve an existing example',
110+
responses: {
111+
'200': {
112+
description: 'Successful response',
113+
},
114+
},
115+
},
116+
},
117+
},
118+
},
119+
services: [
120+
{
121+
name: 'test-service',
122+
url: pulumi.output('https://example.com'),
123+
cloudrun: {
124+
name: 'test',
125+
location: 'us-central-1',
126+
} as unknown,
127+
} as NitricComputeCloudRun,
128+
],
129+
});
130+
});
131+
132+
// Assert its state
133+
it('should create a new Api', async () => {
134+
expect(api?.api).toBeDefined();
135+
await expect(outputToPromise(api?.api.name)).resolves.toEqual('my-gateway-sg');
136+
});
137+
138+
it('should create a new api config', async () => {
139+
expect(api?.config).toBeDefined();
140+
await expect(outputToPromise(api?.config.name)).resolves.toEqual('my-gateway-config-sg');
141+
142+
// asset config has correct parent api
143+
await expect(outputToPromise(api?.config.api)).resolves.toEqual('mock-api-id');
144+
});
145+
146+
it('should create a new gateway', async () => {
147+
expect(api?.gateway).toBeDefined();
148+
await expect(outputToPromise(api?.gateway.name)).resolves.toEqual('my-gateway-gateway-sg');
149+
150+
// Assert gateway has correct config
151+
const configId = await outputToPromise(api?.config.id);
152+
await expect(outputToPromise(api?.gateway.apiConfig)).resolves.toEqual(configId);
153+
});
154+
155+
it('should create a new Iam account', async () => {
156+
expect(api?.invoker).toBeDefined();
157+
await expect(outputToPromise(api?.invoker.name)).resolves.toEqual('my-gateway-acct-sg');
158+
await expect(outputToPromise(api?.invoker.email)).resolves.toEqual('[email protected]');
159+
});
160+
161+
it('should create iam members for the invoker account for each provided service', async () => {
162+
expect(api?.memberships).toHaveLength(1);
163+
164+
// Assert service account wired up correctly
165+
await expect(outputToPromise(api?.memberships[0].member)).resolves.toEqual(
166+
'serviceAccount:[email protected]',
167+
);
168+
});
169+
});
170+
});

packages/plugins/gcp/src/resources/api.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
4343
public readonly hostname: pulumi.Output<string>;
4444
public readonly url: pulumi.Output<string>;
4545

46+
public readonly api: gcp.apigateway.Api;
47+
public readonly config: gcp.apigateway.ApiConfig;
48+
public readonly gateway: gcp.apigateway.Gateway;
49+
public readonly invoker: gcp.serviceaccount.Account;
50+
public readonly memberships: gcp.cloudrun.IamMember[];
51+
4652
constructor(name: string, args: NitricApiGcpApiGatewayArgs, opts?: pulumi.ComponentResourceOptions) {
4753
super('nitric:api:GcpApiGateway', name, {}, opts);
4854
const { api, services } = args;
@@ -54,7 +60,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
5460
const targetServices = Object.keys(api.paths).reduce((svcs, path) => {
5561
const p = api.paths[path] as OpenAPIV2.PathItemObject<NitricAPITarget>;
5662

57-
const services = Object.keys(path)
63+
const s = Object.keys(p)
5864
.filter((k) => METHOD_KEYS.includes(k as method))
5965
.reduce((acc, method) => {
6066
const pathTarget = p[method as method]?.[constants.OAI_NITRIC_TARGET_EXT];
@@ -67,7 +73,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
6773
return acc;
6874
}, svcs);
6975

70-
return svcs;
76+
return s;
7177
}, [] as NitricComputeCloudRun[]);
7278

7379
// Replace Nitric API Extensions with google api gateway extensions
@@ -131,7 +137,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
131137
return Buffer.from(JSON.stringify(transformedApi)).toString('base64');
132138
});
133139

134-
const deployedApi = new gcp.apigateway.Api(
140+
this.api = new gcp.apigateway.Api(
135141
name,
136142
{
137143
apiId: name,
@@ -140,7 +146,7 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
140146
);
141147

142148
// Create a new IAM account for invoking
143-
const apiInvoker = new gcp.serviceaccount.Account(
149+
this.invoker = new gcp.serviceaccount.Account(
144150
`${name}-acct`,
145151
{
146152
// Limit to 30 characters for service account name
@@ -151,13 +157,13 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
151157
);
152158

153159
// Bind that IAM account as a member of all available service targets
154-
targetServices.map((svc) => {
155-
new gcp.cloudrun.IamMember(
156-
`${name}-acct-binding`,
160+
this.memberships = targetServices.map((svc) => {
161+
return new gcp.cloudrun.IamMember(
162+
`${name}-${svc.name}-binding`,
157163
{
158164
service: svc.cloudrun.name,
159165
location: svc.cloudrun.location,
160-
member: pulumi.interpolate`serviceAccount:${apiInvoker.email}`,
166+
member: pulumi.interpolate`serviceAccount:${this.invoker.email}`,
161167
role: 'roles/run.invoker',
162168
},
163169
defaultResourceOptions,
@@ -167,10 +173,10 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
167173
// Now we need to create the document provided and interpolate the deployed service targets
168174
// i.e. their Urls...
169175
// Deploy the config
170-
const deployedConfig = new gcp.apigateway.ApiConfig(
176+
this.config = new gcp.apigateway.ApiConfig(
171177
`${name}-config`,
172178
{
173-
api: deployedApi.apiId,
179+
api: this.api.apiId,
174180
displayName: `${name}-config`,
175181
apiConfigId: `${name}-config`,
176182
openapiDocuments: [
@@ -184,31 +190,36 @@ export class NitricApiGcpApiGateway extends pulumi.ComponentResource {
184190
gatewayConfig: {
185191
backendConfig: {
186192
// Add the service account for the invoker here...
187-
googleServiceAccount: apiInvoker.email,
193+
googleServiceAccount: this.invoker.email,
188194
},
189195
},
190196
},
191197
defaultResourceOptions,
192198
);
193199

194200
// Deploy the gateway
195-
const gateway = new gcp.apigateway.Gateway(
201+
this.gateway = new gcp.apigateway.Gateway(
196202
`${name}-gateway`,
197203
{
198204
displayName: `${name}-gateway`,
199205
gatewayId: `${name}-gateway`,
200-
apiConfig: deployedConfig.id,
206+
apiConfig: this.config.id,
201207
},
202208
defaultResourceOptions,
203209
);
204210

205-
this.hostname = gateway.defaultHostname;
206-
this.url = gateway.defaultHostname.apply((n) => `https://${n}`);
211+
this.hostname = this.gateway.defaultHostname;
212+
this.url = this.gateway.defaultHostname.apply((n) => `https://${n}`);
207213

208214
this.registerOutputs({
209215
name: this.name,
210216
hostname: this.hostname,
211217
url: this.url,
218+
api: this.api,
219+
invoker: this.invoker,
220+
memberships: this.memberships,
221+
config: this.config,
222+
gateway: this.gateway,
212223
});
213224
}
214225

0 commit comments

Comments
 (0)