@mkntz/pulumi-cloudflare-extensions 0.1.0 → 0.1.1
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/README.md +506 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,2 +1,506 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# Pulumi Cloudflare Extensions
|
|
2
|
+
|
|
3
|
+
A comprehensive guide to using the `@mkntz/pulumi-cloudflare-extensions` library for extending Pulumi's Cloudflare provider with additional utilities.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Overview](#overview)
|
|
9
|
+
- [API Reference](#api-reference)
|
|
10
|
+
- [Permission Groups](#permission-groups)
|
|
11
|
+
- [Examples](#examples)
|
|
12
|
+
- [TypeScript Support](#typescript-support)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install the package from npm:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @mkntz/pulumi-cloudflare-extensions
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or with yarn:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add @mkntz/pulumi-cloudflare-extensions
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Prerequisites
|
|
31
|
+
|
|
32
|
+
- Node.js 18.x or later
|
|
33
|
+
- [@pulumi/pulumi](https://www.npmjs.com/package/@pulumi/pulumi) ^3.0.0
|
|
34
|
+
- [@pulumi/cloudflare](https://www.npmjs.com/package/@pulumi/cloudflare) ^5.0.0
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
|
|
40
|
+
The `pulumi-cloudflare-extensions` library provides utility functions that extend the Cloudflare Pulumi provider with convenient helpers for common tasks. Currently, it focuses on simplifying the management and retrieval of Cloudflare API token permission groups.
|
|
41
|
+
|
|
42
|
+
### Key Features
|
|
43
|
+
|
|
44
|
+
- **Structured Permission Groups**: Convert flat Cloudflare API permission lists into an organized, hierarchical structure
|
|
45
|
+
- **Dual API Options**: Support for both async/await and Pulumi Output-based patterns
|
|
46
|
+
- **Type Safety**: Full TypeScript support with comprehensive type definitions
|
|
47
|
+
- **Seamless Integration**: Works naturally with Pulumi's resource and stack architecture
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## API Reference
|
|
52
|
+
|
|
53
|
+
### Permission Groups
|
|
54
|
+
|
|
55
|
+
The permission groups module provides functions to retrieve and structure Cloudflare account API token permission groups. Permission groups are classified by scope: account-level and zone-level permissions.
|
|
56
|
+
|
|
57
|
+
#### `getStructuredPermissionGroups(args, opts?)`
|
|
58
|
+
|
|
59
|
+
**Description**: Asynchronously retrieves permission groups structured by scope.
|
|
60
|
+
|
|
61
|
+
**Arguments:**
|
|
62
|
+
|
|
63
|
+
- `args` (required): `GetStructuredPermissionGroupsArgs`
|
|
64
|
+
- `accountId` (string): Your Cloudflare account ID
|
|
65
|
+
- `opts` (optional): `pulumi.InvokeOptions` - Standard Pulumi invoke options
|
|
66
|
+
|
|
67
|
+
**Returns**: `Promise<StructuredPermissionGroups>`
|
|
68
|
+
|
|
69
|
+
**Return Type:**
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interface StructuredPermissionGroups {
|
|
73
|
+
account: Record<string, { id: string }>;
|
|
74
|
+
zone: Record<string, { id: string }>;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The returned object contains:
|
|
79
|
+
|
|
80
|
+
- `account`: Permission groups with account-level scope (`com.cloudflare.api.account`)
|
|
81
|
+
- `zone`: Permission groups with zone-level scope (`com.cloudflare.api.account.zone`)
|
|
82
|
+
|
|
83
|
+
Each permission group is keyed by its human-readable name with an object containing its `id`.
|
|
84
|
+
|
|
85
|
+
**Use Cases:**
|
|
86
|
+
|
|
87
|
+
- When you need immediate access to permission group data
|
|
88
|
+
- In non-stack contexts where you can use async/await
|
|
89
|
+
- For imperative workflows
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
#### `getStructuredPermissionGroupsOutput(args, opts?)`
|
|
94
|
+
|
|
95
|
+
**Description**: Retrieves permission groups as a Pulumi Output, enabling declarative workflows and composition.
|
|
96
|
+
|
|
97
|
+
**Arguments:**
|
|
98
|
+
|
|
99
|
+
- `args` (required): `GetStructuredPermissionGroupsOutputArgs`
|
|
100
|
+
- `accountId` (pulumi.Input<string>): Your Cloudflare account ID (can be an Output or string)
|
|
101
|
+
- `opts` (optional): `pulumi.InvokeOptions` - Standard Pulumi invoke options
|
|
102
|
+
|
|
103
|
+
**Returns**: `pulumi.Output<StructuredPermissionGroups>`
|
|
104
|
+
|
|
105
|
+
**Use Cases:**
|
|
106
|
+
|
|
107
|
+
- Stack-based Pulumi programs
|
|
108
|
+
- When passing account IDs from other resources
|
|
109
|
+
- For lazy evaluation and composition with other Pulumi resources
|
|
110
|
+
- When you need to reference this in export statements
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
#### Data Structure
|
|
115
|
+
|
|
116
|
+
Both functions return a `StructuredPermissionGroups` object with the following shape:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
{
|
|
120
|
+
"account": {
|
|
121
|
+
"Account Analytics Read": { "id": "abc123def456" },
|
|
122
|
+
"Account Members Read": { "id": "xyz789uvw012" },
|
|
123
|
+
"Account Settings Read": { "id": "..." }
|
|
124
|
+
// ... more account permissions
|
|
125
|
+
},
|
|
126
|
+
"zone": {
|
|
127
|
+
"Zone Read": { "id": "pqr345stu678" },
|
|
128
|
+
"Zone DNS Read": { "id": "..." }
|
|
129
|
+
// ... more zone permissions
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
### Basic Async Usage
|
|
139
|
+
|
|
140
|
+
Retrieve permission groups using async/await:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
144
|
+
import { getStructuredPermissionGroups } from "@mkntz/pulumi-cloudflare-extensions";
|
|
145
|
+
|
|
146
|
+
const accountId = "your-cloudflare-account-id";
|
|
147
|
+
|
|
148
|
+
// In an async context
|
|
149
|
+
const permissionGroups = await getStructuredPermissionGroups({
|
|
150
|
+
accountId,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
console.log("Account permissions:", permissionGroups.account);
|
|
154
|
+
console.log("Zone permissions:", permissionGroups.zone);
|
|
155
|
+
|
|
156
|
+
// Access specific permission
|
|
157
|
+
const accountReadId = permissionGroups.account["Account Analytics Read"]?.id;
|
|
158
|
+
console.log("Account Analytics Read ID:", accountReadId);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Stack-Based Usage with Outputs
|
|
164
|
+
|
|
165
|
+
Use with Pulumi stacks and resources:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
169
|
+
import * as cloudflare from "@pulumi/cloudflare";
|
|
170
|
+
import { getStructuredPermissionGroupsOutput } from "@mkntz/pulumi-cloudflare-extensions";
|
|
171
|
+
|
|
172
|
+
const config = new pulumi.Config();
|
|
173
|
+
const accountId = config.require("accountId");
|
|
174
|
+
|
|
175
|
+
// Get permission groups as Output
|
|
176
|
+
const permissionGroups = getStructuredPermissionGroupsOutput({
|
|
177
|
+
accountId,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Create an API token with specific permissions
|
|
181
|
+
const apiToken = new cloudflare.ApiToken("my-token", {
|
|
182
|
+
accountId,
|
|
183
|
+
name: "My API Token",
|
|
184
|
+
policies: [
|
|
185
|
+
{
|
|
186
|
+
permissionGroups: [
|
|
187
|
+
// Reference permission by name - automatically resolves to ID
|
|
188
|
+
permissionGroups.account["Account Analytics Read"].id,
|
|
189
|
+
permissionGroups.zone["Zone Read"].id,
|
|
190
|
+
],
|
|
191
|
+
resources: {
|
|
192
|
+
"com.cloudflare.api.account.zone": "*",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Export the token
|
|
199
|
+
export const token = apiToken.value;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### Creating API Tokens with Fine-Grained Permissions
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
208
|
+
import * as cloudflare from "@pulumi/cloudflare";
|
|
209
|
+
import { getStructuredPermissionGroupsOutput } from "@mkntz/pulumi-cloudflare-extensions";
|
|
210
|
+
|
|
211
|
+
const config = new pulumi.Config();
|
|
212
|
+
const accountId = config.require("accountId");
|
|
213
|
+
const accountEmail = config.require("accountEmail");
|
|
214
|
+
|
|
215
|
+
// Get structured permissions
|
|
216
|
+
const permissions = getStructuredPermissionGroupsOutput({ accountId });
|
|
217
|
+
|
|
218
|
+
// Create a read-only token for analytics
|
|
219
|
+
const analyticsToken = new cloudflare.ApiToken("analytics-token", {
|
|
220
|
+
accountId,
|
|
221
|
+
name: "Analytics Read-Only Token",
|
|
222
|
+
policies: [
|
|
223
|
+
{
|
|
224
|
+
permissionGroups: [
|
|
225
|
+
permissions.account["Account Analytics Read"].id,
|
|
226
|
+
permissions.zone["Zone Analytics Read"].id,
|
|
227
|
+
],
|
|
228
|
+
resources: {
|
|
229
|
+
"com.cloudflare.api.account.zone": "*",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Create a DNS management token
|
|
236
|
+
const dnsToken = new cloudflare.ApiToken("dns-token", {
|
|
237
|
+
accountId,
|
|
238
|
+
name: "DNS Management Token",
|
|
239
|
+
policies: [
|
|
240
|
+
{
|
|
241
|
+
permissionGroups: [
|
|
242
|
+
permissions.zone["Zone DNS Read"].id,
|
|
243
|
+
permissions.zone["Zone DNS Write"].id,
|
|
244
|
+
],
|
|
245
|
+
resources: {
|
|
246
|
+
"com.cloudflare.api.account.zone": "*",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Create a restrictive token for a specific zone
|
|
253
|
+
const zoneId = config.require("zoneId");
|
|
254
|
+
const restrictiveToken = new cloudflare.ApiToken("restrictive-token", {
|
|
255
|
+
accountId,
|
|
256
|
+
name: "Single Zone Token",
|
|
257
|
+
policies: [
|
|
258
|
+
{
|
|
259
|
+
permissionGroups: [permissions.zone["Zone Read"].id],
|
|
260
|
+
resources: {
|
|
261
|
+
"com.cloudflare.api.account.zone": zoneId,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
export const analyticsTokenValue = analyticsToken.value;
|
|
268
|
+
export const dnsTokenValue = dnsToken.value;
|
|
269
|
+
export const restrictiveTokenValue = restrictiveToken.value;
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### Debugging and Inspection
|
|
275
|
+
|
|
276
|
+
Find available permissions:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { getStructuredPermissionGroups } from "@mkntz/pulumi-cloudflare-extensions";
|
|
280
|
+
|
|
281
|
+
async function listAllPermissions() {
|
|
282
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!;
|
|
283
|
+
const groups = await getStructuredPermissionGroups({ accountId });
|
|
284
|
+
|
|
285
|
+
console.log("=== Account-Level Permissions ===");
|
|
286
|
+
Object.entries(groups.account).forEach(([name, { id }]) => {
|
|
287
|
+
console.log(` ${name}: ${id}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
console.log("\n=== Zone-Level Permissions ===");
|
|
291
|
+
Object.entries(groups.zone).forEach(([name, { id }]) => {
|
|
292
|
+
console.log(` ${name}: ${id}`);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
listAllPermissions().catch(console.error);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### Combining with Configuration
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
305
|
+
import * as cloudflare from "@pulumi/cloudflare";
|
|
306
|
+
import { getStructuredPermissionGroupsOutput } from "@mkntz/pulumi-cloudflare-extensions";
|
|
307
|
+
|
|
308
|
+
const config = new pulumi.Config();
|
|
309
|
+
const accountId = config.require("accountId");
|
|
310
|
+
const requiredPermissions = config.requireObject<string[]>("permissions");
|
|
311
|
+
|
|
312
|
+
const permissions = getStructuredPermissionGroupsOutput({ accountId });
|
|
313
|
+
|
|
314
|
+
// Dynamically create tokens based on configuration
|
|
315
|
+
const tokens = requiredPermissions.map((permName, index) => {
|
|
316
|
+
return new cloudflare.ApiToken(`token-${index}`, {
|
|
317
|
+
accountId,
|
|
318
|
+
name: `Token for ${permName}`,
|
|
319
|
+
policies: [
|
|
320
|
+
{
|
|
321
|
+
// Permission can be from either account or zone scope
|
|
322
|
+
permissionGroups: pulumi
|
|
323
|
+
.all([
|
|
324
|
+
permissions.account[permName]?.id,
|
|
325
|
+
permissions.zone[permName]?.id,
|
|
326
|
+
])
|
|
327
|
+
.apply(([accountPerm, zonePerm]) => {
|
|
328
|
+
const id = accountPerm || zonePerm;
|
|
329
|
+
if (!id) throw new Error(`Permission '${permName}' not found`);
|
|
330
|
+
return [id];
|
|
331
|
+
}),
|
|
332
|
+
resources: {
|
|
333
|
+
"com.cloudflare.api.account.zone": "*",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
export const tokenValues = tokens.map((t) => t.value);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## TypeScript Support
|
|
346
|
+
|
|
347
|
+
This library is fully typed with TypeScript and provides comprehensive type definitions.
|
|
348
|
+
|
|
349
|
+
### Available Types
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Main data structure
|
|
353
|
+
export interface StructuredPermissionGroups {
|
|
354
|
+
account: Record<string, { id: string }>;
|
|
355
|
+
zone: Record<string, { id: string }>;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Async function arguments
|
|
359
|
+
export interface GetStructuredPermissionGroupsArgs {
|
|
360
|
+
accountId: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Output function arguments (supports Pulumi Inputs)
|
|
364
|
+
export interface GetStructuredPermissionGroupsOutputArgs {
|
|
365
|
+
accountId: pulumi.Input<string>;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Type Safety Example
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
373
|
+
import {
|
|
374
|
+
getStructuredPermissionGroupsOutput,
|
|
375
|
+
StructuredPermissionGroups,
|
|
376
|
+
} from "@mkntz/pulumi-cloudflare-extensions";
|
|
377
|
+
|
|
378
|
+
const config = new pulumi.Config();
|
|
379
|
+
const accountId = config.require("accountId");
|
|
380
|
+
|
|
381
|
+
// Fully typed
|
|
382
|
+
const permissions: pulumi.Output<StructuredPermissionGroups> =
|
|
383
|
+
getStructuredPermissionGroupsOutput({ accountId });
|
|
384
|
+
|
|
385
|
+
// TypeScript knows the structure
|
|
386
|
+
permissions.apply((p) => {
|
|
387
|
+
Object.entries(p.account).forEach(([name, { id }]) => {
|
|
388
|
+
console.log(name, id); // Both typed as strings
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Error Handling
|
|
396
|
+
|
|
397
|
+
### Async Pattern
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import { getStructuredPermissionGroups } from "@mkntz/pulumi-cloudflare-extensions";
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const permissions = await getStructuredPermissionGroups({
|
|
404
|
+
accountId: "invalid-id",
|
|
405
|
+
});
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error("Failed to fetch permission groups:", error);
|
|
408
|
+
// Handle error appropriately
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Output Pattern
|
|
413
|
+
|
|
414
|
+
Errors in Output-based calls are handled through Pulumi's standard error propagation:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
418
|
+
import { getStructuredPermissionGroupsOutput } from "@mkntz/pulumi-cloudflare-extensions";
|
|
419
|
+
|
|
420
|
+
const permissions = getStructuredPermissionGroupsOutput({
|
|
421
|
+
accountId: config.require("accountId"),
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Errors will be caught during pulumi up/preview
|
|
425
|
+
export const report = permissions.apply((p) => {
|
|
426
|
+
// This only executes if the API call succeeds
|
|
427
|
+
return `Found ${Object.keys(p.account).length} account permissions`;
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Best Practices
|
|
434
|
+
|
|
435
|
+
1. **Cache Results**: If calling the same function multiple times, consider caching the output to avoid redundant API calls.
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const permissions = getStructuredPermissionGroupsOutput({ accountId });
|
|
439
|
+
// Reuse `permissions` across multiple resources
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
2. **Validate Permissions**: Always verify that expected permissions exist before using them.
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
permissions.apply((p) => {
|
|
446
|
+
if (!p.account["Account Analytics Read"]) {
|
|
447
|
+
throw new Error("Required permission not found");
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
3. **Use Configuration Files**: Store sensitive data like account IDs in Pulumi configuration.
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
pulumi config set accountId <your-account-id>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
4. **Prefer Output Pattern in Stacks**: Use `getStructuredPermissionGroupsOutput` in Pulumi stack programs for better composition and dependency tracking.
|
|
459
|
+
|
|
460
|
+
5. **Minimal Permissions**: Only request permissions that are actually needed for your use case.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Troubleshooting
|
|
465
|
+
|
|
466
|
+
### "Permission not found" Errors
|
|
467
|
+
|
|
468
|
+
If you get an error about a permission not being found:
|
|
469
|
+
|
|
470
|
+
1. Verify the exact permission name (case-sensitive)
|
|
471
|
+
2. Check that your account ID is correct
|
|
472
|
+
3. Use the debugging example above to list all available permissions
|
|
473
|
+
|
|
474
|
+
### API Call Failures
|
|
475
|
+
|
|
476
|
+
If API calls are failing:
|
|
477
|
+
|
|
478
|
+
1. Ensure your Cloudflare credentials are properly configured
|
|
479
|
+
2. Verify the account ID is valid
|
|
480
|
+
3. Check that your account has the necessary API access
|
|
481
|
+
4. Review Cloudflare API documentation for any rate limits
|
|
482
|
+
|
|
483
|
+
### TypeScript Errors
|
|
484
|
+
|
|
485
|
+
Ensure you're using compatible versions:
|
|
486
|
+
|
|
487
|
+
- `@pulumi/pulumi` version 3.0.0 or later
|
|
488
|
+
- `@pulumi/cloudflare` version 5.0.0 or later
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Contributing
|
|
493
|
+
|
|
494
|
+
Found a bug or have a feature request? Please open an issue on [GitHub](https://github.com/mkntz/pulumi-cloudflare-extensions/issues).
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## License
|
|
499
|
+
|
|
500
|
+
MIT - See [LICENSE](./LICENSE) file for details
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Support
|
|
505
|
+
|
|
506
|
+
For questions and discussions, please open an issue on the [GitHub repository](https://github.com/mkntz/pulumi-cloudflare-extensions).
|