githubEdit

ADR-0005: Provider Trait for Cloud Abstraction

Status

Accepted

Date

2025-01

Context

Spuff aims to support multiple cloud providers:

  • DigitalOcean (current)

  • Hetzner Cloud (planned)

  • AWS EC2 (planned)

  • Others (future)

Each provider has different:

  • API endpoints and authentication

  • Resource naming (droplets, servers, instances)

  • Region and size identifiers

  • Snapshot mechanisms

  • Rate limits and quirks

Requirements

  • Support multiple cloud providers

  • Consistent CLI experience regardless of provider

  • Easy to add new providers

  • Provider-specific features accessible when needed

  • Clean separation of concerns

Decision

We will use a Rust trait (Provider) as an abstraction layer over cloud providers.

The Provider Trait

Factory Pattern

Common Data Types

Consequences

Positive

  • Extensibility: New providers just implement the trait

  • Consistency: CLI code doesn't change for different providers

  • Testability: Mock providers for testing

  • Type safety: Rust compiler enforces interface compliance

  • Documentation: Trait documents the required API

Negative

  • Lowest common denominator: Trait methods must work across all providers

  • Feature gaps: Provider-specific features harder to expose

  • Abstraction leak: Some provider differences may leak through

  • Maintenance: Must update all providers for trait changes

Neutral

  • Requires understanding of Rust traits and dynamic dispatch

  • Some boxing overhead (negligible for network operations)

Design Decisions

Why async_trait?

Cloud API calls are async, and Rust traits don't natively support async methods. The async_trait crate provides this capability.

Why Box<dyn Provider>?

Dynamic dispatch allows runtime provider selection based on configuration. The alternative (generics) would require compile-time provider choice.

Why Send + Sync?

The provider may be used across async tasks. These bounds ensure thread safety.

Method Granularity

Methods are designed to be:

  • Atomic: Each method does one thing

  • Composable: Higher-level operations built from primitives

  • Idempotent where possible: Delete already-deleted resources shouldn't error

Alternatives Considered

Alternative 1: No Abstraction

Direct provider API calls throughout the codebase.

Pros:

  • Full access to provider features

  • No abstraction overhead

Cons:

  • Code duplication

  • Provider-specific code everywhere

  • Hard to add new providers

Why rejected: Not scalable for multi-cloud support.

Alternative 2: Enum-Based Dispatch

Use an enum with match statements:

Pros:

  • No dynamic dispatch

  • Exhaustive matching

Cons:

  • Every match statement must handle all providers

  • Adding provider touches many files

Why rejected: Too invasive when adding providers.

Alternative 3: Generic Parameters

Use generic parameters instead of trait objects:

Pros:

  • No boxing overhead

  • Monomorphization

Cons:

  • Can't select provider at runtime

  • Larger binary (code per provider)

Why rejected: Need runtime provider selection from config.

Alternative 4: gRPC/Plugin System

Load providers as separate processes/plugins.

Pros:

  • True isolation

  • Dynamic loading

Cons:

  • Massive complexity

  • IPC overhead

  • Deployment complexity

Why rejected: Overkill for this use case.

Future Considerations

Provider-Specific Extensions

For provider-specific features, we can:

  1. Downcast: Cast Box<dyn Provider> to concrete type

  2. Extension traits: Additional traits for specific capabilities

  3. Feature flags: Optional trait methods with default implementations

API Versioning

If trait changes significantly:

  1. Create new trait version (e.g., ProviderV2)

  2. Provide adapter from old to new

  3. Deprecate old trait gradually

References

Last updated

Was this helpful?