hemmer-provider-sdk 0.2.0

Rust SDK for building Hemmer providers
Documentation

Hemmer Provider SDK

Crates.io Documentation CI License

Rust SDK for building Hemmer providers

The Hemmer Provider SDK provides gRPC protocol types and server helpers for building providers that integrate with Hemmer, the next-generation Infrastructure as Code tool.

Status

🚧 Currently in active development

Features

  • Complete Provider Protocol: Full gRPC protocol for infrastructure management
  • Schema Support: Define resource and data source schemas with types, validation, and documentation
  • Pre-compiled Types: Committed Rust types (no build-time proto generation required)
  • Server Helpers: Easy provider startup with handshake protocol
  • Validation: Built-in support for provider, resource, and data source config validation
  • State Management: Support for state upgrades and resource imports

Installation

Add to your Cargo.toml:

[dependencies]
hemmer-provider-sdk = "0.1"
tokio = { version = "1", features = ["full"] }
serde_json = "1"

Quick Start

use hemmer_provider_sdk::{
    serve, ProviderService, ProviderError, PlanResult,
    schema::{ProviderSchema, Schema, Attribute, Diagnostic},
};

struct MyProvider;

#[hemmer_provider_sdk::async_trait]
impl ProviderService for MyProvider {
    fn schema(&self) -> ProviderSchema {
        ProviderSchema::new()
            .with_provider_config(
                Schema::v0()
                    .with_attribute("api_key", Attribute::required_string().sensitive())
            )
            .with_resource("mycloud_instance", Schema::v0()
                .with_attribute("name", Attribute::required_string())
                .with_attribute("size", Attribute::optional_string())
                .with_attribute("id", Attribute::computed_string())
            )
    }

    async fn configure(
        &self,
        config: serde_json::Value,
    ) -> Result<Vec<Diagnostic>, ProviderError> {
        // Initialize provider with credentials
        Ok(vec![])
    }

    async fn plan(
        &self,
        resource_type: &str,
        prior_state: Option<serde_json::Value>,
        proposed_state: serde_json::Value,
        config: serde_json::Value,
    ) -> Result<PlanResult, ProviderError> {
        Ok(PlanResult::no_change(proposed_state))
    }

    async fn create(
        &self,
        resource_type: &str,
        planned_state: serde_json::Value,
    ) -> Result<serde_json::Value, ProviderError> {
        // Create the resource and return its state
        Ok(planned_state)
    }

    async fn read(
        &self,
        resource_type: &str,
        current_state: serde_json::Value,
    ) -> Result<serde_json::Value, ProviderError> {
        // Read current state from the API
        Ok(current_state)
    }

    async fn update(
        &self,
        resource_type: &str,
        prior_state: serde_json::Value,
        planned_state: serde_json::Value,
    ) -> Result<serde_json::Value, ProviderError> {
        // Update the resource
        Ok(planned_state)
    }

    async fn delete(
        &self,
        resource_type: &str,
        current_state: serde_json::Value,
    ) -> Result<(), ProviderError> {
        // Delete the resource
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    serve(MyProvider).await
}

Provider Protocol

The SDK implements a complete provider protocol with the following RPCs:

RPC Purpose
GetMetadata Returns provider capabilities and resource/data source names
GetSchema Returns full schema for provider config, resources, and data sources
ValidateProviderConfig Validates provider configuration before use
Configure Configures provider with credentials and settings
Stop Gracefully shuts down the provider
ValidateResourceConfig Validates resource configuration before planning
UpgradeResourceState Migrates state from older schema versions
Plan Calculates required changes to reach desired state
Create Creates a new resource
Read Reads current state of a resource
Update Updates an existing resource
Delete Deletes a resource
ImportResourceState Imports existing infrastructure
ValidateDataSourceConfig Validates data source configuration
ReadDataSource Reads data from external sources

Schema Types

Define schemas for your resources using the builder pattern:

use hemmer_provider_sdk::schema::*;

let schema = Schema::v0()
    // Required string attribute
    .with_attribute("name", Attribute::required_string()
        .with_description("The name of the resource"))

    // Optional with default
    .with_attribute("region", Attribute::optional_string()
        .with_default(serde_json::json!("us-east-1")))

    // Computed (read-only) attribute
    .with_attribute("id", Attribute::computed_string())

    // Sensitive attribute (hidden in logs)
    .with_attribute("password", Attribute::required_string().sensitive())

    // Force replacement when changed
    .with_attribute("ami", Attribute::required_string().with_force_new())

    // Nested blocks
    .with_block("network", NestedBlock::list(
        Block::new()
            .with_attribute("subnet_id", Attribute::required_string())
            .with_attribute("security_groups",
                Attribute::new(AttributeType::list(AttributeType::String),
                              AttributeFlags::optional()))
    ));

Validation

The SDK provides built-in validation helpers to validate configuration values against schemas:

use hemmer_provider_sdk::{validate, is_valid, schema::Schema};
use serde_json::json;

let schema = Schema::v0()
    .with_attribute("name", Attribute::required_string())
    .with_attribute("count", Attribute::optional(AttributeType::Number));

let value = json!({
    "name": "my-resource",
    "count": 5
});

// Get detailed validation diagnostics
let diagnostics = validate(&schema, &value);
if diagnostics.is_empty() {
    println!("Configuration is valid!");
}

// Or use the simple boolean check
if is_valid(&schema, &value) {
    println!("Valid!");
}

Testing

The SDK includes a test harness for provider implementations:

use hemmer_provider_sdk::{ProviderTester, ProviderService};
use serde_json::json;

#[tokio::test]
async fn test_resource_lifecycle() {
    let provider = MyProvider::new();
    let tester = ProviderTester::new(provider);

    // Test complete CRUD lifecycle
    let final_state = tester
        .lifecycle_crud(
            "mycloud_instance",
            json!({"name": "test"}),           // create config
            json!({"name": "test-updated"}),   // update config
        )
        .await
        .expect("lifecycle should succeed");

    // Or test individual operations with assertions
    let plan = tester
        .plan("mycloud_instance", None, json!({"name": "test"}))
        .await
        .unwrap();

    tester.assert_plan_creates(&plan);
}

Handshake Protocol

When a provider starts via serve(), it outputs a handshake string to stdout:

HEMMER_PROVIDER|1|127.0.0.1:50051

Format: HEMMER_PROVIDER|<protocol_version>|<address>

This allows Hemmer to spawn the provider as a subprocess and connect via gRPC.

Protocol Versioning

The SDK implements protocol version negotiation to ensure compatibility between Hemmer and providers built with different SDK versions.

Version Constants

use hemmer_provider_sdk::{PROTOCOL_VERSION, MIN_PROTOCOL_VERSION, check_protocol_version};

// Current protocol version (incremented for breaking changes)
assert_eq!(PROTOCOL_VERSION, 1);

// Minimum supported version for backwards compatibility
assert_eq!(MIN_PROTOCOL_VERSION, 1);

Version Negotiation

During the GetSchema RPC, Hemmer sends its protocol version, and the provider validates compatibility:

// Automatically handled by the SDK
// Providers reject clients with versions below MIN_PROTOCOL_VERSION
check_protocol_version(client_version)?;

Versioning Strategy

  • Patch (0.1.x): Bug fixes, no protocol changes
  • Minor (0.x.0): Additive changes (new optional fields), backwards compatible
  • Major (x.0.0): Breaking changes, increment PROTOCOL_VERSION

When PROTOCOL_VERSION is incremented, consider whether older clients should still be supported by setting MIN_PROTOCOL_VERSION appropriately.

Contributing

Quick Setup

# Clone and setup development environment
git clone https://github.com/hemmer-io/hemmer-provider-sdk
cd hemmer-provider-sdk
./scripts/setup.sh

The setup script will:

  • Install git pre-commit hooks
  • Verify your Rust toolchain
  • Run an initial build and test

Development Workflow

# Build
cargo build

# Run tests
cargo test

# Run linter
cargo clippy --all-targets -- -D warnings

# Format code
cargo fmt --all

# View documentation
cargo doc --no-deps --open

Regenerating Proto Types

Proto types are pre-compiled and committed. To regenerate after changing proto/provider.proto:

# Requires protoc to be installed
cargo build --features regenerate-proto

Code Style

  • Follow standard Rust conventions (rustfmt, clippy)
  • Document public APIs with doc comments
  • Write tests for new functionality
  • Keep commits focused and atomic

Pull Request Process

  1. Create a feature branch from main
  2. Make your changes with tests
  3. Ensure all checks pass (cargo test, cargo clippy, cargo fmt --check)
  4. Submit a PR using the template
  5. Address review feedback

Related Projects

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.