@mkntz/pulumi-cloudflare-extensions 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +506 -2
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,2 +1,506 @@
1
- # pulumi-cloudflare-extensions
2
- Extensions to Pulumi Cloudflare provider
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkntz/pulumi-cloudflare-extensions",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Extensions to Pulumi Cloudflare provider",
5
5
  "keywords": [
6
6
  "pulumi",