Skip to main content

Documentation Index

Fetch the complete documentation index at: https://platform.docs.zenoo.com/llms.txt

Use this file to discover all available pages before exploring further.

Implementing Cloud Providers

Guide for implementing support for new cloud providers (Azure, GCP, or custom infrastructure).

Overview

Adding a new cloud provider requires:
  1. Implementing cloud-provider-api interfaces
  2. Mapping provider exceptions to CloudProviderException
  3. Creating Spring Boot autoconfiguration
  4. Writing tests

Reference Implementations

Before implementing a new provider, review existing implementations:

Local Provider (Simple Reference)

Module: cloud-provider-local The local provider is the simplest complete implementation and serves as an excellent reference: Advantages as a reference:
  • Single module (easier to understand)
  • No external dependencies (focus on core interfaces)
  • Simple storage (ConcurrentHashMap)
  • Clear error handling patterns
  • Well-commented code
  • Complete test coverage
Key classes to study:
cloud-provider-local/
├── store/
│   ├── LocalComponentStore.java          # ComponentStore implementation
│   ├── LocalApiKeyStore.java             # ApiKeyStore implementation
│   └── LocalSharableStore.java           # SharableStore implementation
├── secret/
│   └── LocalApiKeySecretStorage.java     # ApiKeySecretStorage implementation
├── config/
│   ├── LocalComponentConfigStorage.java  # ComponentConfigStorage implementation
│   └── LocalComponentConfigReader.java   # Synchronous config reader
├── support/
│   └── LocalStoreSupport.java            # Base class with error handling
└── autoconfigure/
    ├── LocalCloudProviderAutoConfiguration.java
    ├── LocalProviderConfig.java
    └── SharableCleanupScheduler.java
Example: Simple ComponentStore:
@Service
public class LocalComponentStore extends LocalStoreSupport
                                 implements ComponentStore {

    private final ConcurrentHashMap<String, StringComponent> components =
        new ConcurrentHashMap<>();

    @Override
    public Mono<StringComponent> get(ComponentId id) {
        return wrapWithErrorHandling(
            Mono.fromCallable(() -> {
                ComponentId resolvedId = resolveLatestRevision(id);
                StringComponent component = components.get(toKey(resolvedId));
                if (component == null) {
                    throw new NoSuchElementException("Component not found: " + id);
                }
                return component;
            }),
            "get component",
            id.getName()
        );
    }

    // Error handling in base class (LocalStoreSupport)
    protected Throwable wrapException(Throwable throwable) {
        if (throwable instanceof NoSuchElementException) {
            return new StorageException(
                throwable.getMessage(),
                throwable,
                CloudProviderException.ErrorType.NOT_FOUND
            );
        }
        // ... other mappings
    }
}
Study these patterns:
  1. Error Handling - LocalStoreSupport.wrapWithErrorHandling()
  2. Exception Mapping - Java exceptions -> CloudProviderException
  3. Reactive Wrapping - Mono.fromCallable() for synchronous operations
  4. LATEST Resolution - resolveLatestRevision() pattern
  5. Auto-Configuration - Conditional bean creation

AWS Provider (Production Reference)

Module: cloud-provider-aws (4 submodules) The AWS provider is a production-ready implementation showing advanced patterns: Advantages as a reference:
  • Real-world external service integration (DynamoDB, Secrets Manager)
  • Advanced error handling and retries
  • Multi-region support
  • Metrics publishing (CloudWatch)
  • Production-grade testing
Module structure:
cloud-provider-aws/
├── aws-stores/              # DynamoDB storage implementations
├── aws-secrets/             # Secrets Manager implementations
├── aws-metrics/             # CloudWatch metrics
└── aws-spring-boot-starter/ # Auto-configuration
Study these patterns:
  1. SDK Integration - AWS SDK v2 async client usage
  2. Retry Logic - Exponential backoff for transient errors
  3. Connection Pooling - Managing client lifecycle
  4. Metric Publishing - CloudWatch integration
  5. Multi-Module Structure - When to split providers
View AWS provider documentation

Choosing a Reference

AspectLocal ProviderAWS Provider
ComplexitySimpleComplex
Lines of Code~1,500~5,000
External DepsNoneAWS SDK, DynamoDB, Secrets Manager
Module StructureSingle4 submodules
Best for LearningCore patternsProduction patterns
Best for StartingYesUse as second reference
Recommendation: Start with the local provider to understand core patterns, then review AWS provider for production considerations.

Module Structure

Create a new module following the pattern:
cloud-provider-{name}/
├── {name}-stores/
│   └── src/main/java/com/zenoo/hub/cloudprovider/{name}/stores/
├── {name}-secrets/
│   └── src/main/java/com/zenoo/hub/cloudprovider/{name}/secrets/
├── {name}-metrics/
│   └── src/main/java/com/zenoo/hub/cloudprovider/{name}/metrics/
└── {name}-spring-boot-starter/
    └── src/main/java/com/zenoo/hub/cloudprovider/{name}/autoconfigure/

Implementing Storage Interfaces

ComponentStore Implementation

@Service
public class AzureComponentStore implements ComponentStore {

    private final CosmosClient cosmosClient;

    @Override
    public Mono<StringComponent> get(ComponentId id) {
        return Mono.defer(() -> {
            try {
                var response = cosmosClient
                    .getDatabase(databaseName)
                    .getContainer(containerName)
                    .readItem(id.getFullName(),
                             new PartitionKey(id.getName()),
                             ComponentRecord.class);
                return Mono.just(response.getItem().toStringComponent());
            } catch (CosmosException e) {
                return Mono.error(mapException(e));
            }
        });
    }

    private Throwable mapException(CosmosException e) {
        if (e.getStatusCode() == 404) {
            return new StorageException(
                "Component not found",
                e,
                CloudProviderException.ErrorType.NOT_FOUND
            );
        }
        return new StorageException(
            "Storage error",
            e,
            CloudProviderException.ErrorType.SERVICE_ERROR
        );
    }
}

SharableStore Implementation

@Service
public class AzureSharableStore implements SharableStore {

    @Override
    public Mono<SharableRecord> get(String token) {
        // Implementation using Azure Cosmos DB or Table Storage
    }

    @Override
    public Mono<String> store(SharableRecord record) {
        // Implementation with TTL support
    }
}

Implementing Configuration Interfaces

ComponentConfigStorage Implementation

@Service
public class AzureComponentConfigStorage implements ComponentConfigStorage {

    private final SecretClient secretClient;

    @Override
    public Mono<String> get(ComponentConfigId configId) {
        return Mono.defer(() -> {
            try {
                KeyVaultSecret secret = secretClient.getSecret(
                    buildSecretName(configId)
                );
                return Mono.just(secret.getValue());
            } catch (ResourceNotFoundException e) {
                return Mono.empty();
            } catch (Exception e) {
                return Mono.error(mapException(e));
            }
        });
    }
}

Spring Boot Autoconfiguration

Main Configuration Class

@AutoConfiguration
@ConditionalOnClass(CosmosClient.class)
@ConditionalOnProperty(
    name = "hub.cloud.provider.type",
    havingValue = "azure"
)
@Import({
    AzureStoresAutoConfiguration.class,
    AzureSecretsAutoConfiguration.class
})
public class AzureCloudProviderAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public CosmosClient cosmosClient(AzureConfig config) {
        return new CosmosClientBuilder()
            .endpoint(config.getEndpoint())
            .key(config.getKey())
            .buildClient();
    }
}

Register Autoconfiguration

Create META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.zenoo.hub.cloudprovider.azure.autoconfigure.AzureCloudProviderAutoConfiguration

Configuration Properties

@Data
@Validated
@ConfigurationProperties(prefix = "hub.azure")
public class AzureConfig {
    @NotBlank
    private String endpoint;

    @NotBlank
    private String key;

    @NotBlank
    private String databaseName;
}

Testing

Unit Tests

@ExtendWith(MockitoExtension.class)
class AzureComponentStoreTest {

    @Mock
    private CosmosClient cosmosClient;

    private AzureComponentStore store;

    @Test
    void shouldGetComponent() {
        // Test implementation
    }
}

Integration Tests

Use Test Containers or Azure emulator for integration tests.

Example: Azure Provider Structure

// Configuration
@ConfigurationProperties("hub.azure")
public class AzureConfig {
    String endpoint;
    String key;
}

// Component Store
@Service
public class AzureComponentStore implements ComponentStore {
    // Cosmos DB implementation
}

// Secrets Management
@Service
public class AzureKeyVaultStorage implements ApiKeySecretStorage {
    // Key Vault implementation
}

// Autoconfiguration
@AutoConfiguration
@ConditionalOnProperty(name = "hub.cloud.provider.type", havingValue = "azure")
public class AzureCloudProviderAutoConfiguration {
    // Bean definitions
}

See Also