Pulumi's Node.js SDK for infrastructure-as-code platform that allows you to create, deploy, and manage infrastructure using familiar programming languages and tools.
85
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.
class dynamic.Resource extends CustomResource {
constructor(
provider: ResourceProvider<Inputs, Outputs>,
name: string,
props: Inputs,
opts?: CustomResourceOptions,
remote?: boolean
);
}
interface ResourceProvider<Inputs, Outputs> {
check?(inputs: any): Promise<CheckResult<Inputs>>;
create?(inputs: Inputs): Promise<CreateResult<Outputs>>;
read?(id: string, props?: any): Promise<ReadResult<Outputs>>;
update?(id: string, olds: Outputs, news: Inputs): Promise<UpdateResult<Outputs>>;
delete?(id: string, props: Outputs): Promise<void>;
diff?(id: string, olds: Outputs, news: Inputs): Promise<DiffResult>;
}interface CheckResult<Inputs> {
inputs?: Inputs;
failures?: CheckFailure[];
}
interface CreateResult<Outputs> {
id: string;
outs?: Outputs;
}
interface ReadResult<Outputs> {
id?: string;
outs?: Outputs;
}
interface UpdateResult<Outputs> {
outs?: Outputs;
}
interface DiffResult {
changes?: boolean;
replaces?: string[];
stables?: string[];
deleteBeforeReplace?: boolean;
}
interface CheckFailure {
property: string;
reason: string;
}import * as pulumi from "@pulumi/pulumi";
import axios from "axios";
interface ApiResourceInputs {
name: string;
config: any;
apiEndpoint: string;
apiToken: string;
}
interface ApiResourceOutputs {
id: string;
name: string;
status: string;
createdAt: string;
}
const apiResourceProvider: pulumi.dynamic.ResourceProvider<ApiResourceInputs, ApiResourceOutputs> = {
async create(inputs) {
const response = await axios.post(`${inputs.apiEndpoint}/resources`, {
name: inputs.name,
config: inputs.config,
}, {
headers: {
'Authorization': `Bearer ${inputs.apiToken}`,
'Content-Type': 'application/json',
},
});
return {
id: response.data.id,
outs: {
id: response.data.id,
name: response.data.name,
status: response.data.status,
createdAt: response.data.createdAt,
},
};
},
async read(id, props) {
const inputs = props as ApiResourceInputs;
try {
const response = await axios.get(`${inputs.apiEndpoint}/resources/${id}`, {
headers: {
'Authorization': `Bearer ${inputs.apiToken}`,
},
});
return {
id: response.data.id,
outs: {
id: response.data.id,
name: response.data.name,
status: response.data.status,
createdAt: response.data.createdAt,
},
};
} catch (error) {
if (error.response?.status === 404) {
return { id: undefined, outs: undefined };
}
throw error;
}
},
async update(id, olds, news) {
const response = await axios.put(`${news.apiEndpoint}/resources/${id}`, {
name: news.name,
config: news.config,
}, {
headers: {
'Authorization': `Bearer ${news.apiToken}`,
'Content-Type': 'application/json',
},
});
return {
outs: {
id: response.data.id,
name: response.data.name,
status: response.data.status,
createdAt: response.data.createdAt,
},
};
},
async delete(id, props) {
const inputs = props as ApiResourceOutputs;
await axios.delete(`${(props as any).apiEndpoint}/resources/${id}`, {
headers: {
'Authorization': `Bearer ${(props as any).apiToken}`,
},
});
},
};
// Use the dynamic resource
const config = new pulumi.Config();
const apiResource = new pulumi.dynamic.Resource("my-api-resource", apiResourceProvider, "my-resource", {
name: "my-custom-resource",
config: {
setting1: "value1",
setting2: config.getNumber("setting2") || 42,
},
apiEndpoint: config.require("apiEndpoint"),
apiToken: config.requireSecret("apiToken"),
});
export const resourceId = apiResource.id;
export const resourceStatus = apiResource.outs.apply(o => o?.status);import * as pulumi from "@pulumi/pulumi";
import { Client } from "pg";
interface MigrationInputs {
connectionString: string;
migrationScript: string;
version: string;
}
interface MigrationOutputs {
version: string;
appliedAt: string;
checksum: string;
}
const migrationProvider: pulumi.dynamic.ResourceProvider<MigrationInputs, MigrationOutputs> = {
async create(inputs) {
const client = new Client({ connectionString: inputs.connectionString });
await client.connect();
try {
// Create migrations table if it doesn't exist
await client.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT NOW(),
checksum VARCHAR(255)
)
`);
// Calculate checksum
const crypto = require('crypto');
const checksum = crypto.createHash('md5').update(inputs.migrationScript).digest('hex');
// Check if migration already applied
const existing = await client.query(
'SELECT * FROM schema_migrations WHERE version = $1',
[inputs.version]
);
if (existing.rows.length > 0) {
if (existing.rows[0].checksum !== checksum) {
throw new Error(`Migration ${inputs.version} checksum mismatch`);
}
// Already applied, return existing record
return {
id: inputs.version,
outs: {
version: existing.rows[0].version,
appliedAt: existing.rows[0].applied_at.toISOString(),
checksum: existing.rows[0].checksum,
},
};
}
// Apply migration
await client.query(inputs.migrationScript);
// Record migration
const result = await client.query(
'INSERT INTO schema_migrations (version, checksum) VALUES ($1, $2) RETURNING *',
[inputs.version, checksum]
);
return {
id: inputs.version,
outs: {
version: result.rows[0].version,
appliedAt: result.rows[0].applied_at.toISOString(),
checksum: result.rows[0].checksum,
},
};
} finally {
await client.end();
}
},
async read(id, props) {
const inputs = props as MigrationInputs;
const client = new Client({ connectionString: inputs.connectionString });
await client.connect();
try {
const result = await client.query(
'SELECT * FROM schema_migrations WHERE version = $1',
[id]
);
if (result.rows.length === 0) {
return { id: undefined, outs: undefined };
}
return {
id: result.rows[0].version,
outs: {
version: result.rows[0].version,
appliedAt: result.rows[0].applied_at.toISOString(),
checksum: result.rows[0].checksum,
},
};
} finally {
await client.end();
}
},
async delete(id, props) {
// Migrations typically aren't rolled back automatically
console.log(`Migration ${id} marked for deletion but not rolled back`);
},
};
// Use the migration resource
const dbConnectionString = new pulumi.Config().requireSecret("dbConnectionString");
const migration = new pulumi.dynamic.Resource("user-table-migration", migrationProvider, "v001_create_users", {
connectionString: dbConnectionString,
version: "v001_create_users",
migrationScript: `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
`,
});import * as pulumi from "@pulumi/pulumi";
import * as fs from "fs";
import * as path from "path";
interface FileResourceInputs {
filePath: string;
content: string;
permissions?: string;
}
interface FileResourceOutputs {
filePath: string;
size: number;
checksum: string;
permissions: string;
}
const fileProvider: pulumi.dynamic.ResourceProvider<FileResourceInputs, FileResourceOutputs> = {
async check(inputs) {
const failures: pulumi.dynamic.CheckFailure[] = [];
if (!inputs.filePath) {
failures.push({ property: "filePath", reason: "filePath is required" });
}
if (!inputs.content) {
failures.push({ property: "content", reason: "content is required" });
}
return { inputs, failures };
},
async create(inputs) {
const filePath = path.resolve(inputs.filePath);
const dir = path.dirname(filePath);
// Ensure directory exists
fs.mkdirSync(dir, { recursive: true });
// Write file
fs.writeFileSync(filePath, inputs.content);
// Set permissions if specified
if (inputs.permissions) {
fs.chmodSync(filePath, inputs.permissions);
}
// Get file stats
const stats = fs.statSync(filePath);
const crypto = require('crypto');
const checksum = crypto.createHash('md5').update(inputs.content).digest('hex');
return {
id: filePath,
outs: {
filePath: filePath,
size: stats.size,
checksum: checksum,
permissions: stats.mode.toString(8),
},
};
},
async read(id, props) {
try {
if (!fs.existsSync(id)) {
return { id: undefined, outs: undefined };
}
const content = fs.readFileSync(id, 'utf8');
const stats = fs.statSync(id);
const crypto = require('crypto');
const checksum = crypto.createHash('md5').update(content).digest('hex');
return {
id: id,
outs: {
filePath: id,
size: stats.size,
checksum: checksum,
permissions: stats.mode.toString(8),
},
};
} catch (error) {
return { id: undefined, outs: undefined };
}
},
async update(id, olds, news) {
// Write updated content
fs.writeFileSync(id, news.content);
// Update permissions if changed
if (news.permissions && news.permissions !== olds.permissions) {
fs.chmodSync(id, news.permissions);
}
// Get updated stats
const stats = fs.statSync(id);
const crypto = require('crypto');
const checksum = crypto.createHash('md5').update(news.content).digest('hex');
return {
outs: {
filePath: id,
size: stats.size,
checksum: checksum,
permissions: stats.mode.toString(8),
},
};
},
async delete(id, props) {
if (fs.existsSync(id)) {
fs.unlinkSync(id);
}
},
async diff(id, olds, news) {
const changes = olds.content !== news.content || olds.permissions !== news.permissions;
return { changes };
},
};
// Use the file resource
const configFile = new pulumi.dynamic.Resource("app-config", fileProvider, "config-file", {
filePath: "./deployed-config.json",
content: JSON.stringify({
environment: pulumi.getStack(),
timestamp: new Date().toISOString(),
}, null, 2),
permissions: "0644",
});Install with Tessl CLI
npx tessl i tessl/npm-pulumi--pulumidocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10