CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-pulumi--pulumi

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

1.02x
Overview
Eval results
Files

dynamic-resources.mddocs/

Dynamic Resources

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.

Core Dynamic Resource Classes

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>;
}

Provider Result Types

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;
}

Usage Examples

Simple REST API Resource

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);

Database Migration Resource

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);
  `,
});

File System Resource

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",
});

Best Practices

  • Implement proper error handling in all provider methods
  • Use appropriate HTTP status codes and error messages
  • Implement idempotent operations (create should handle existing resources)
  • Use checksums or version tracking for update detection
  • Implement proper cleanup in delete operations
  • Validate inputs in the check method
  • Handle external API rate limiting and retries
  • Use secrets for sensitive configuration like API tokens
  • Consider using diff method for more efficient updates
  • Test dynamic resources thoroughly in development environments
  • Document expected inputs and outputs clearly
  • Handle partial failures gracefully
  • Use appropriate timeouts for external API calls

Install with Tessl CLI

npx tessl i tessl/npm-pulumi--pulumi

docs

asset-management.md

automation.md

configuration.md

dynamic-resources.md

index.md

logging-diagnostics.md

output-system.md

provider-development.md

resource-management.md

runtime-operations.md

stack-references.md

utilities.md

tile.json