Skip to content

Commit 728f0ce

Browse files
authored
Added a Pulumi example with Wrangler and a dynamic provider
1 parent 9550f64 commit 728f0ce

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
---
2+
title: Manage not supported resources or tasks with your Pulumi script
3+
pcx_content_type: tutorial
4+
updated: 2024-11-08
5+
sidebar:
6+
order: 5
7+
---
8+
9+
import { TabItem, Tabs } from "~/components";
10+
11+
Below you will find a complete example of using Pulumi to create a zone and other types of resources.
12+
13+
In this example besides a Zone we also create a Worker, a Zero Trust Applications, Zero Trust Policies, a D1 database among others.
14+
15+
Check also how to:
16+
17+
- create Workers by calling Wrangler directly, instead of using the Cloudflare Pulumi provider supported Worker resource. The advantage of this method is that you can use it for any other deployment related task like executing D1 migrations as seen in the example below. When running the IaC script, Wrangler will create or update Workers and D1 (also performing migrations). Observe that D1 migrations state in Pulumi is only changed when the migrations directory hash changes triggering that Pulumi action.
18+
- create a dynamic resource provider for a resource not supported. In this example we demonstrate how to do it for Vectorize.
19+
20+
21+
```
22+
23+
"use strict";
24+
25+
const pulumi = require("@pulumi/pulumi");
26+
const cloudflare = require("@pulumi/cloudflare");
27+
const command = require("@pulumi/command");
28+
const path = require('path');
29+
const axios = require("axios");
30+
const https = require('https');
31+
const crypto = require("crypto");
32+
const fs = require("fs");
33+
34+
// Load configuration
35+
const config = new pulumi.Config();
36+
const domainName = config.require("domainName");
37+
const accountId = config.require("accountId");
38+
const apiToken = config.requireSecret("apiToken");
39+
40+
// Function to compute hash of a file
41+
function computeFileHashSync(filePath) {
42+
const fileBuffer = fs.readFileSync(filePath);
43+
const hash = crypto.createHash('sha256');
44+
hash.update(fileBuffer);
45+
return hash.digest('hex');
46+
}
47+
48+
// Function to compute the hash of a directory
49+
async function hashDirectory(dirPath) {
50+
const files = await fs.promises.readdir(dirPath);
51+
const fileHashes = [];
52+
for (const file of files) {
53+
const filePath = path.join(dirPath, file);
54+
const fileStat = await fs.promises.stat(filePath);
55+
if (fileStat.isFile()) {
56+
const fileData = await fs.promises.readFile(filePath);
57+
const hash = crypto.createHash('sha256').update(fileData).digest('hex');
58+
fileHashes.push(hash);
59+
}
60+
}
61+
// Combine all file hashes and hash the result to get a unique hash for the directory
62+
const combinedHash = crypto.createHash('sha256').update(fileHashes.join('')).digest('hex');
63+
return combinedHash;
64+
}
65+
66+
// Instantiate Cloudflare provider
67+
// https://www.pulumi.com/registry/packages/cloudflare/
68+
//--------------------------------------------------------------------------------------------------------------------
69+
const cloudflareProvider = new cloudflare.Provider("cloudflare", {
70+
apiToken: apiToken
71+
});
72+
73+
// Create a Cloudflare Zone
74+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zone/
75+
//--------------------------------------------------------------------------------------------------------------------
76+
const myZone = new cloudflare.Zone("myZone", {
77+
zone: domainName,
78+
plan: "enterprise",
79+
accountId: accountId
80+
}, { provider: cloudflareProvider });
81+
82+
// Create a Cloudflare Queue (used as a binding in Worker)
83+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/queue/
84+
//--------------------------------------------------------------------------------------------------------------------
85+
const myqueue = new cloudflare.Queue("myqueue", {
86+
zoneId: myZone.id,
87+
name: "myqueue",
88+
description: "Queue for my messages",
89+
accountId: accountId
90+
}, { provider: cloudflareProvider });
91+
92+
// Create a Cloudflare Queue (used as a binding in Worker)
93+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/queue/
94+
//--------------------------------------------------------------------------------------------------------------------
95+
const myqueuedeadletter = new cloudflare.Queue("myqueuedeadletter", {
96+
zoneId: myZone.id,
97+
name: "myqueuedeadletter",
98+
description: "Queue for messages that were not processed correctly",
99+
accountId: accountId
100+
}, { provider: cloudflareProvider });
101+
102+
// Create a D1 Database
103+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/d1database/
104+
//--------------------------------------------------------------------------------------------------------------------
105+
const myD1Database = new cloudflare.D1Database("myD1Database", {
106+
accountId: accountId,
107+
name: "mydb",
108+
}, { provider: cloudflareProvider });
109+
110+
// Deploy Changes to D1 Schema
111+
// Cloudflare Wrangler stores list of migrations in the D1 database.
112+
// To check which migrations were ran go to the Cloudflare dashboard and run "select * from d1_migrations" on the console of the D1 database
113+
//--------------------------------------------------------------------------------------------------------------------
114+
let d1Dir="../../mydb/";
115+
const d1Migration = new command.local.Command("d1Migration", {
116+
dir: d1Dir,
117+
create: `npx wrangler d1 migrations apply mydb --remote`,
118+
triggers:[
119+
hashDirectory(`${d1Dir}migrations`)
120+
]
121+
}, { dependsOn: [myD1Database] });
122+
123+
// Run 'wrangler' command
124+
// https://www.pulumi.com/registry/packages/command/api-docs/local/command/
125+
//--------------------------------------------------------------------------------------------------------------------
126+
let workerDir="../../worker-test/";
127+
const workerTest = new command.local.Command("worker-test", {
128+
dir: workerDir,
129+
create: "npx wrangler deploy",
130+
triggers: [ // A unique trigger vector to force recreation
131+
computeFileHashSync(`${workerDir}src/index.js`),
132+
computeFileHashSync(`${workerDir}wrangler.toml`)]
133+
}, { dependsOn: [myZone, myqueue, myqueuedeadletter, myD1Database] });
134+
135+
// Create "Add" group Service Auth Token
136+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessservicetoken/
137+
//--------------------------------------------------------------------------------------------------------------------
138+
const myServiceToken = new cloudflare.ZeroTrustAccessServiceToken("myServiceToken", {
139+
zoneId: myZone.id,
140+
name: "myServiceToken"
141+
}, { provider: cloudflareProvider });
142+
143+
// Create an Access "Add" Group
144+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessgroup/
145+
//--------------------------------------------------------------------------------------------------------------------
146+
const myAccessGroup = new cloudflare.ZeroTrustAccessGroup("myAccessGroup", {
147+
accountId: accountId,
148+
name: "myAccessGroup",
149+
// Define the group criteria (e.g., email domains, identity providers, etc.)
150+
// This example adds users from the specified email domain.
151+
includes: [
152+
{ serviceTokens: [myServiceToken.id] }
153+
]
154+
}, { provider: cloudflareProvider, dependsOn: [myServiceToken] });
155+
156+
// Create an Access App for "Add"
157+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessapplication/
158+
//--------------------------------------------------------------------------------------------------------------------
159+
const myAccessApp = new cloudflare.ZeroTrustAccessApplication("myAccessApp", {
160+
zoneId: myZone.id,
161+
name: "myApp",
162+
domain: `myapp.${domainName}`,
163+
sessionDuration: "24h"
164+
}, { provider: cloudflareProvider, dependsOn: [myAccessGroup, myZone] });
165+
166+
// Create an Access App with Allow Policy for Access "Add" Group
167+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccesspolicy/
168+
//--------------------------------------------------------------------------------------------------------------------
169+
const myAddAccessPolicy = new cloudflare.ZeroTrustAccessPolicy("myAccessPolicy", {
170+
zoneId: myZone.id,
171+
applicationId: myAccessApp.id,
172+
name: "myAccessPolicy",
173+
decision: "allow",
174+
precedence: 1,
175+
includes: [
176+
{
177+
groups: [myAccessGroup.id]
178+
}
179+
],
180+
}, { provider: cloudflareProvider, dependsOn: [myAccessApp] });
181+
182+
// Create a Vectorize Index
183+
//--------------------------------------------------------------------------------------------------------------------
184+
// Define a dynamic provider for Vectorize because the Cloudflare Pulumi provider does not support this resource yet
185+
const VectorizeIndexDynamicCloudflareProvider = {
186+
async create(inputs) {
187+
// Create an instance of the HTTPS Agent with SSL verification disabled to avoid WARP issues
188+
const httpsAgent = new https.Agent({
189+
rejectUnauthorized: false
190+
});
191+
const url = `https://api.cloudflare.com/client/v4/accounts/${inputs.accountId}/vectorize/v2/indexes`;
192+
const data = {
193+
config: {dimensions: 768, metric: 'cosine'},
194+
description: inputs.description,
195+
name: inputs.name
196+
};
197+
// Headers
198+
const options = {
199+
httpsAgent,
200+
headers: {
201+
'Content-Type': 'application/json',
202+
'Authorization': `Bearer ${inputs.apiToken}`
203+
}
204+
};
205+
// Make an API call to create the resource
206+
const response = await axios.post(url, data, options);
207+
const resourceId = inputs.name // For now we use the Vectorize index name as id, because Vectorize does not provide an id for it
208+
// Return the ID and output values
209+
return { id: resourceId,
210+
outs: {
211+
name: inputs.name,
212+
accountId: inputs.accountId,
213+
apiToken: inputs.apiToken
214+
} };
215+
},
216+
217+
async delete(id, props) {
218+
// Create an instance of the HTTPS Agent with SSL verification disabled to avoid WARP issues
219+
const httpsAgent = new https.Agent({
220+
rejectUnauthorized: false
221+
});
222+
const url=`https://api.cloudflare.com/client/v4/accounts/${props.accountId}/vectorize/v2/indexes/${id}`;
223+
// Headers
224+
const options = {
225+
httpsAgent,
226+
headers: {
227+
'Content-Type': 'application/json',
228+
'Authorization': `Bearer ${props.apiToken}`
229+
}
230+
};
231+
// Make an API call to delete the resource
232+
await axios.delete(url, options);
233+
},
234+
235+
async update(id, oldInputs, newInputs) {
236+
// Vectorize once created does not allow updates
237+
}
238+
};
239+
240+
// Define a dynamic resource
241+
class VectorizeIndex extends pulumi.dynamic.Resource {
242+
constructor(name, args, opts) {
243+
super(VectorizeIndexDynamicCloudflareProvider, name, args, opts);
244+
}
245+
}
246+
247+
// Use the dynamic resource in your Pulumi stack
248+
// Don't change properties after creation. Currently, Vectorize does not allow changes.
249+
// To delete this resource just remove or comment this block of code
250+
const my_vectorize_index = new VectorizeIndex("myvectorizeindex", {
251+
name: "myvectorize_index",
252+
accountId: accountId,
253+
namespaceId: myZone.id, // Set appropriate namespace id
254+
vectorDimensions: 768, // Example, adjust dimensions as needed
255+
apiToken: apiToken
256+
});
257+
258+
// Export relevant outputs
259+
// Access these outputs after Pulumi has ran using:
260+
// $ pulumi stack output
261+
// $ pulumi stack output zoneId
262+
//--------------------------------------------------------------------------------------------------------------------
263+
exports.zoneId = myZone.id;
264+
exports.myqueueId = myqueue.id
265+
exports.myqueuedeadletter = myqueuedeadletter.id
266+
exports.myD1DatabaseId = myD1Database.id
267+
exports.workerTestId = workerTest.id
268+
exports.myServiceToken = myServiceToken.id
269+
exports.myServiceTokenClientId = myAddServiceToken.clientId
270+
exports.myServiceTokenClientSecret = myAddServiceToken.clientSecret
271+
272+
```
273+
274+
## Accessing Pulumi exports
275+
276+
After running your Pulumi script with "pulumi up" you will have your resources created or updated.
277+
278+
The script above also exports outputs that you can access from other tools that you want or need to integrate into your deployment pipeline.
279+
280+
For this, you can use:
281+
282+
```
283+
$ pulumi stack output myServiceTokenClientSecret --show-secrets
284+
```
285+
286+

0 commit comments

Comments
 (0)