or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

asset-management.mdautomation.mdconfiguration.mddynamic-resources.mdindex.mdlogging-diagnostics.mdoutput-system.mdprovider-development.mdresource-management.mdruntime-operations.mdstack-references.mdutilities.md

dynamic-resources.mddocs/

0

# Dynamic Resources

1

2

Dynamic resources enable creating custom Pulumi resources in TypeScript/JavaScript without building a full provider plugin, perfect for integrating with APIs and services not covered by existing providers.

3

4

## Core Dynamic Resource Classes

5

6

```typescript { .api }

7

class dynamic.Resource extends CustomResource {

8

constructor(

9

provider: ResourceProvider<Inputs, Outputs>,

10

name: string,

11

props: Inputs,

12

opts?: CustomResourceOptions,

13

remote?: boolean

14

);

15

}

16

17

interface ResourceProvider<Inputs, Outputs> {

18

check?(inputs: any): Promise<CheckResult<Inputs>>;

19

create?(inputs: Inputs): Promise<CreateResult<Outputs>>;

20

read?(id: string, props?: any): Promise<ReadResult<Outputs>>;

21

update?(id: string, olds: Outputs, news: Inputs): Promise<UpdateResult<Outputs>>;

22

delete?(id: string, props: Outputs): Promise<void>;

23

diff?(id: string, olds: Outputs, news: Inputs): Promise<DiffResult>;

24

}

25

```

26

27

## Provider Result Types

28

29

```typescript { .api }

30

interface CheckResult<Inputs> {

31

inputs?: Inputs;

32

failures?: CheckFailure[];

33

}

34

35

interface CreateResult<Outputs> {

36

id: string;

37

outs?: Outputs;

38

}

39

40

interface ReadResult<Outputs> {

41

id?: string;

42

outs?: Outputs;

43

}

44

45

interface UpdateResult<Outputs> {

46

outs?: Outputs;

47

}

48

49

interface DiffResult {

50

changes?: boolean;

51

replaces?: string[];

52

stables?: string[];

53

deleteBeforeReplace?: boolean;

54

}

55

56

interface CheckFailure {

57

property: string;

58

reason: string;

59

}

60

```

61

62

## Usage Examples

63

64

### Simple REST API Resource

65

66

```typescript

67

import * as pulumi from "@pulumi/pulumi";

68

import axios from "axios";

69

70

interface ApiResourceInputs {

71

name: string;

72

config: any;

73

apiEndpoint: string;

74

apiToken: string;

75

}

76

77

interface ApiResourceOutputs {

78

id: string;

79

name: string;

80

status: string;

81

createdAt: string;

82

}

83

84

const apiResourceProvider: pulumi.dynamic.ResourceProvider<ApiResourceInputs, ApiResourceOutputs> = {

85

async create(inputs) {

86

const response = await axios.post(`${inputs.apiEndpoint}/resources`, {

87

name: inputs.name,

88

config: inputs.config,

89

}, {

90

headers: {

91

'Authorization': `Bearer ${inputs.apiToken}`,

92

'Content-Type': 'application/json',

93

},

94

});

95

96

return {

97

id: response.data.id,

98

outs: {

99

id: response.data.id,

100

name: response.data.name,

101

status: response.data.status,

102

createdAt: response.data.createdAt,

103

},

104

};

105

},

106

107

async read(id, props) {

108

const inputs = props as ApiResourceInputs;

109

110

try {

111

const response = await axios.get(`${inputs.apiEndpoint}/resources/${id}`, {

112

headers: {

113

'Authorization': `Bearer ${inputs.apiToken}`,

114

},

115

});

116

117

return {

118

id: response.data.id,

119

outs: {

120

id: response.data.id,

121

name: response.data.name,

122

status: response.data.status,

123

createdAt: response.data.createdAt,

124

},

125

};

126

} catch (error) {

127

if (error.response?.status === 404) {

128

return { id: undefined, outs: undefined };

129

}

130

throw error;

131

}

132

},

133

134

async update(id, olds, news) {

135

const response = await axios.put(`${news.apiEndpoint}/resources/${id}`, {

136

name: news.name,

137

config: news.config,

138

}, {

139

headers: {

140

'Authorization': `Bearer ${news.apiToken}`,

141

'Content-Type': 'application/json',

142

},

143

});

144

145

return {

146

outs: {

147

id: response.data.id,

148

name: response.data.name,

149

status: response.data.status,

150

createdAt: response.data.createdAt,

151

},

152

};

153

},

154

155

async delete(id, props) {

156

const inputs = props as ApiResourceOutputs;

157

158

await axios.delete(`${(props as any).apiEndpoint}/resources/${id}`, {

159

headers: {

160

'Authorization': `Bearer ${(props as any).apiToken}`,

161

},

162

});

163

},

164

};

165

166

// Use the dynamic resource

167

const config = new pulumi.Config();

168

169

const apiResource = new pulumi.dynamic.Resource("my-api-resource", apiResourceProvider, "my-resource", {

170

name: "my-custom-resource",

171

config: {

172

setting1: "value1",

173

setting2: config.getNumber("setting2") || 42,

174

},

175

apiEndpoint: config.require("apiEndpoint"),

176

apiToken: config.requireSecret("apiToken"),

177

});

178

179

export const resourceId = apiResource.id;

180

export const resourceStatus = apiResource.outs.apply(o => o?.status);

181

```

182

183

### Database Migration Resource

184

185

```typescript

186

import * as pulumi from "@pulumi/pulumi";

187

import { Client } from "pg";

188

189

interface MigrationInputs {

190

connectionString: string;

191

migrationScript: string;

192

version: string;

193

}

194

195

interface MigrationOutputs {

196

version: string;

197

appliedAt: string;

198

checksum: string;

199

}

200

201

const migrationProvider: pulumi.dynamic.ResourceProvider<MigrationInputs, MigrationOutputs> = {

202

async create(inputs) {

203

const client = new Client({ connectionString: inputs.connectionString });

204

await client.connect();

205

206

try {

207

// Create migrations table if it doesn't exist

208

await client.query(`

209

CREATE TABLE IF NOT EXISTS schema_migrations (

210

version VARCHAR(255) PRIMARY KEY,

211

applied_at TIMESTAMP DEFAULT NOW(),

212

checksum VARCHAR(255)

213

)

214

`);

215

216

// Calculate checksum

217

const crypto = require('crypto');

218

const checksum = crypto.createHash('md5').update(inputs.migrationScript).digest('hex');

219

220

// Check if migration already applied

221

const existing = await client.query(

222

'SELECT * FROM schema_migrations WHERE version = $1',

223

[inputs.version]

224

);

225

226

if (existing.rows.length > 0) {

227

if (existing.rows[0].checksum !== checksum) {

228

throw new Error(`Migration ${inputs.version} checksum mismatch`);

229

}

230

// Already applied, return existing record

231

return {

232

id: inputs.version,

233

outs: {

234

version: existing.rows[0].version,

235

appliedAt: existing.rows[0].applied_at.toISOString(),

236

checksum: existing.rows[0].checksum,

237

},

238

};

239

}

240

241

// Apply migration

242

await client.query(inputs.migrationScript);

243

244

// Record migration

245

const result = await client.query(

246

'INSERT INTO schema_migrations (version, checksum) VALUES ($1, $2) RETURNING *',

247

[inputs.version, checksum]

248

);

249

250

return {

251

id: inputs.version,

252

outs: {

253

version: result.rows[0].version,

254

appliedAt: result.rows[0].applied_at.toISOString(),

255

checksum: result.rows[0].checksum,

256

},

257

};

258

} finally {

259

await client.end();

260

}

261

},

262

263

async read(id, props) {

264

const inputs = props as MigrationInputs;

265

const client = new Client({ connectionString: inputs.connectionString });

266

await client.connect();

267

268

try {

269

const result = await client.query(

270

'SELECT * FROM schema_migrations WHERE version = $1',

271

[id]

272

);

273

274

if (result.rows.length === 0) {

275

return { id: undefined, outs: undefined };

276

}

277

278

return {

279

id: result.rows[0].version,

280

outs: {

281

version: result.rows[0].version,

282

appliedAt: result.rows[0].applied_at.toISOString(),

283

checksum: result.rows[0].checksum,

284

},

285

};

286

} finally {

287

await client.end();

288

}

289

},

290

291

async delete(id, props) {

292

// Migrations typically aren't rolled back automatically

293

console.log(`Migration ${id} marked for deletion but not rolled back`);

294

},

295

};

296

297

// Use the migration resource

298

const dbConnectionString = new pulumi.Config().requireSecret("dbConnectionString");

299

300

const migration = new pulumi.dynamic.Resource("user-table-migration", migrationProvider, "v001_create_users", {

301

connectionString: dbConnectionString,

302

version: "v001_create_users",

303

migrationScript: `

304

CREATE TABLE users (

305

id SERIAL PRIMARY KEY,

306

email VARCHAR(255) UNIQUE NOT NULL,

307

created_at TIMESTAMP DEFAULT NOW()

308

);

309

CREATE INDEX idx_users_email ON users(email);

310

`,

311

});

312

```

313

314

### File System Resource

315

316

```typescript

317

import * as pulumi from "@pulumi/pulumi";

318

import * as fs from "fs";

319

import * as path from "path";

320

321

interface FileResourceInputs {

322

filePath: string;

323

content: string;

324

permissions?: string;

325

}

326

327

interface FileResourceOutputs {

328

filePath: string;

329

size: number;

330

checksum: string;

331

permissions: string;

332

}

333

334

const fileProvider: pulumi.dynamic.ResourceProvider<FileResourceInputs, FileResourceOutputs> = {

335

async check(inputs) {

336

const failures: pulumi.dynamic.CheckFailure[] = [];

337

338

if (!inputs.filePath) {

339

failures.push({ property: "filePath", reason: "filePath is required" });

340

}

341

342

if (!inputs.content) {

343

failures.push({ property: "content", reason: "content is required" });

344

}

345

346

return { inputs, failures };

347

},

348

349

async create(inputs) {

350

const filePath = path.resolve(inputs.filePath);

351

const dir = path.dirname(filePath);

352

353

// Ensure directory exists

354

fs.mkdirSync(dir, { recursive: true });

355

356

// Write file

357

fs.writeFileSync(filePath, inputs.content);

358

359

// Set permissions if specified

360

if (inputs.permissions) {

361

fs.chmodSync(filePath, inputs.permissions);

362

}

363

364

// Get file stats

365

const stats = fs.statSync(filePath);

366

const crypto = require('crypto');

367

const checksum = crypto.createHash('md5').update(inputs.content).digest('hex');

368

369

return {

370

id: filePath,

371

outs: {

372

filePath: filePath,

373

size: stats.size,

374

checksum: checksum,

375

permissions: stats.mode.toString(8),

376

},

377

};

378

},

379

380

async read(id, props) {

381

try {

382

if (!fs.existsSync(id)) {

383

return { id: undefined, outs: undefined };

384

}

385

386

const content = fs.readFileSync(id, 'utf8');

387

const stats = fs.statSync(id);

388

const crypto = require('crypto');

389

const checksum = crypto.createHash('md5').update(content).digest('hex');

390

391

return {

392

id: id,

393

outs: {

394

filePath: id,

395

size: stats.size,

396

checksum: checksum,

397

permissions: stats.mode.toString(8),

398

},

399

};

400

} catch (error) {

401

return { id: undefined, outs: undefined };

402

}

403

},

404

405

async update(id, olds, news) {

406

// Write updated content

407

fs.writeFileSync(id, news.content);

408

409

// Update permissions if changed

410

if (news.permissions && news.permissions !== olds.permissions) {

411

fs.chmodSync(id, news.permissions);

412

}

413

414

// Get updated stats

415

const stats = fs.statSync(id);

416

const crypto = require('crypto');

417

const checksum = crypto.createHash('md5').update(news.content).digest('hex');

418

419

return {

420

outs: {

421

filePath: id,

422

size: stats.size,

423

checksum: checksum,

424

permissions: stats.mode.toString(8),

425

},

426

};

427

},

428

429

async delete(id, props) {

430

if (fs.existsSync(id)) {

431

fs.unlinkSync(id);

432

}

433

},

434

435

async diff(id, olds, news) {

436

const changes = olds.content !== news.content || olds.permissions !== news.permissions;

437

return { changes };

438

},

439

};

440

441

// Use the file resource

442

const configFile = new pulumi.dynamic.Resource("app-config", fileProvider, "config-file", {

443

filePath: "./deployed-config.json",

444

content: JSON.stringify({

445

environment: pulumi.getStack(),

446

timestamp: new Date().toISOString(),

447

}, null, 2),

448

permissions: "0644",

449

});

450

```

451

452

## Best Practices

453

454

- Implement proper error handling in all provider methods

455

- Use appropriate HTTP status codes and error messages

456

- Implement idempotent operations (create should handle existing resources)

457

- Use checksums or version tracking for update detection

458

- Implement proper cleanup in delete operations

459

- Validate inputs in the check method

460

- Handle external API rate limiting and retries

461

- Use secrets for sensitive configuration like API tokens

462

- Consider using diff method for more efficient updates

463

- Test dynamic resources thoroughly in development environments

464

- Document expected inputs and outputs clearly

465

- Handle partial failures gracefully

466

- Use appropriate timeouts for external API calls