refactor: introduce provider registries for cloud and platform decoupling#174
refactor: introduce provider registries for cloud and platform decoupling#174
Conversation
Replace the monolithic switch statement in resolveCredentials() with a
pluggable registry. Each provider/credType pair is now an independent
CloudCredentialResolver registered via init(). Provider-specific logic
is extracted into cloud_account_{aws_creds,gcp,azure,do,k8s}.go; the
godo import moves to cloud_account_do.go alongside the DO resolvers.
cloud_account.go retains only the CloudAccount struct, Init, and the
mock/registry dispatch logic. All 22 existing tests pass unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… modules Replace switch statements in Init() for platform.networking, platform.dns, platform.autoscaling, platform.apigateway, platform.kubernetes, and platform.ecs with registry lookups. Each module now exposes a public Register*Backend() function and a *BackendFactory type, with mock and real backends registered in init(). New backends can be added by external packages without modifying core module files. All existing tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request refactors the workflow engine to introduce registry patterns for cloud credential resolvers and platform backend factories. The goal is to decouple provider-specific logic from core modules, enabling better extensibility and plugin support.
Changes:
- Introduced
CloudCredentialResolverinterface and global registry for managing cloud provider credential resolution logic, replacing large switch statements with a pluggable resolver pattern - Added backend factory registries for all platform modules (Kubernetes, ECS, DNS, Networking, Autoscaling, API Gateway), enabling dynamic registration of provider-specific backends
- Extracted provider-specific credential resolution logic from
cloud_account.gointo separate files (aws, gcp, azure, digitalocean, kubernetes)
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| module/cloud_credential_resolver.go | New file defining CloudCredentialResolver interface and global registry for credential resolvers |
| module/cloud_account.go | Refactored to use registry-based credential resolution, removing ~260 lines of switch-case logic |
| module/cloud_account_aws_creds.go | New file with AWS credential resolvers (static, env, profile, role_arn) |
| module/cloud_account_gcp.go | New file with GCP credential resolvers (static, env, service account variants, workload identity) |
| module/cloud_account_azure.go | New file with Azure credential resolvers (static, env, client credentials, managed identity, CLI) |
| module/cloud_account_do.go | New file with DigitalOcean credential resolvers and client helper |
| module/cloud_account_k8s.go | New file with Kubernetes credential resolvers (static, env, kubeconfig) |
| module/platform_kubernetes.go | Added KubernetesBackendFactory registry to replace switch statement for backend selection |
| module/platform_kubernetes_kind.go | Registers all Kubernetes backends (kind, k3s, eks, gke, aks) in init() |
| module/platform_networking.go | Added NetworkingBackendFactory registry pattern |
| module/platform_dns.go | Added DNSBackendFactory registry pattern |
| module/platform_ecs.go | Added ECSBackendFactory registry with backward-compatible fallback to mock |
| module/platform_autoscaling.go | Added AutoscalingBackendFactory registry pattern |
| module/platform_apigateway.go | Added APIGatewayBackendFactory registry pattern |
| func init() { | ||
| RegisterCredentialResolver(&doStaticResolver{}) | ||
| RegisterCredentialResolver(&doEnvResolver{}) | ||
| RegisterCredentialResolver(&doAPITokenResolver{}) | ||
| } | ||
|
|
||
| // doStaticResolver resolves DigitalOcean credentials from static config fields. | ||
| type doStaticResolver struct{} | ||
|
|
||
| func (r *doStaticResolver) Provider() string { return "digitalocean" } | ||
| func (r *doStaticResolver) CredentialType() string { return "static" } | ||
|
|
||
| func (r *doStaticResolver) Resolve(m *CloudAccount) error { | ||
| credsMap, _ := m.config["credentials"].(map[string]any) | ||
| if credsMap != nil { | ||
| m.creds.Token, _ = credsMap["token"].(string) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // doEnvResolver resolves DigitalOcean credentials from environment variables. | ||
| type doEnvResolver struct{} | ||
|
|
||
| func (r *doEnvResolver) Provider() string { return "digitalocean" } | ||
| func (r *doEnvResolver) CredentialType() string { return "env" } | ||
|
|
||
| func (r *doEnvResolver) Resolve(m *CloudAccount) error { | ||
| m.creds.Token = os.Getenv("DIGITALOCEAN_TOKEN") | ||
| if m.creds.Token == "" { | ||
| m.creds.Token = os.Getenv("DO_TOKEN") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // doAPITokenResolver resolves a DigitalOcean API token from explicit config. | ||
| type doAPITokenResolver struct{} | ||
|
|
||
| func (r *doAPITokenResolver) Provider() string { return "digitalocean" } | ||
| func (r *doAPITokenResolver) CredentialType() string { return "api_token" } | ||
|
|
||
| func (r *doAPITokenResolver) Resolve(m *CloudAccount) error { | ||
| credsMap, _ := m.config["credentials"].(map[string]any) | ||
| if credsMap == nil { | ||
| return fmt.Errorf("api_token credential requires 'token'") | ||
| } | ||
| token, _ := credsMap["token"].(string) | ||
| if token == "" { | ||
| return fmt.Errorf("api_token credential requires 'token'") | ||
| } | ||
| m.creds.Token = token | ||
| return nil | ||
| } | ||
|
|
||
| // doClient returns a configured *godo.Client using the Token credential. | ||
| // The caller must have resolved credentials with provider=digitalocean before calling this. | ||
| func (m *CloudAccount) doClient() (*godo.Client, error) { | ||
| if m.creds == nil || m.creds.Token == "" { | ||
| return nil, fmt.Errorf("cloud.account %q: DigitalOcean token not set", m.name) | ||
| } | ||
| ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m.creds.Token}) | ||
| httpClient := oauth2.NewClient(context.Background(), ts) | ||
| return godo.NewClient(httpClient), nil | ||
| } |
There was a problem hiding this comment.
The new DigitalOcean credential resolvers (doStaticResolver, doEnvResolver, doAPITokenResolver) introduced in this file lack test coverage. All other cloud providers (AWS, GCP, Azure, Kubernetes) have comprehensive tests in cloud_account_test.go, but DigitalOcean is missing.
Add test cases similar to TestCloudAccount_Azure_ClientCredentials and TestCloudAccount_Azure_EnvVars to verify the DigitalOcean resolvers work correctly.
| // RegisterCredentialResolver registers a CloudCredentialResolver in the global registry. | ||
| // It is safe to call from init() functions. | ||
| func RegisterCredentialResolver(r CloudCredentialResolver) { | ||
| p := r.Provider() | ||
| if credentialResolvers[p] == nil { | ||
| credentialResolvers[p] = map[string]CloudCredentialResolver{} | ||
| } | ||
| credentialResolvers[p][r.CredentialType()] = r | ||
| } |
There was a problem hiding this comment.
The global registry credentialResolvers is accessed without synchronization. While the comment states "It is safe to call from init() functions", it doesn't protect against concurrent access if someone were to call RegisterCredentialResolver at runtime (outside of init) while other goroutines are reading from the registry in resolveCredentials.
Consider adding either: (1) a sync.RWMutex to protect concurrent access, or (2) a more explicit comment stating that this function MUST ONLY be called from init() functions and doing so at runtime will cause race conditions.
| // NetworkingBackendFactory creates a networkBackend for a given provider config. | ||
| type NetworkingBackendFactory func(cfg map[string]any) (networkBackend, error) | ||
|
|
||
| // networkingBackendRegistry maps provider name to its factory. | ||
| var networkingBackendRegistry = map[string]NetworkingBackendFactory{} | ||
|
|
||
| // RegisterNetworkingBackend registers a NetworkingBackendFactory for the given provider name. | ||
| func RegisterNetworkingBackend(provider string, factory NetworkingBackendFactory) { | ||
| networkingBackendRegistry[provider] = factory | ||
| } |
There was a problem hiding this comment.
The global backend registries (kubernetesBackendRegistry, ecsBackendRegistry, etc.) are accessed without synchronization. While all current registrations happen in init() functions (which run serially), these registries lack synchronization protection. If external plugins or runtime code were to call these registration functions concurrently with module initialization, race conditions could occur.
Consider adding a sync.RWMutex to protect concurrent access, similar to how it's done in other parts of the codebase (e.g., module/api_workflow_ui.go:24 uses sync.RWMutex). This would make the pattern more robust and safer for future plugin extensibility.
| // KubernetesBackendFactory creates a kubernetesBackend for a given cluster type config. | ||
| type KubernetesBackendFactory func(cfg map[string]any) (kubernetesBackend, error) | ||
|
|
||
| // kubernetesBackendRegistry maps cluster type name to its factory. | ||
| var kubernetesBackendRegistry = map[string]KubernetesBackendFactory{} | ||
|
|
||
| // RegisterKubernetesBackend registers a KubernetesBackendFactory for the given cluster type. | ||
| func RegisterKubernetesBackend(clusterType string, factory KubernetesBackendFactory) { | ||
| kubernetesBackendRegistry[clusterType] = factory | ||
| } |
There was a problem hiding this comment.
The global backend registries (kubernetesBackendRegistry, ecsBackendRegistry, etc.) are accessed without synchronization. While all current registrations happen in init() functions (which run serially), these registries lack synchronization protection. If external plugins or runtime code were to call these registration functions concurrently with module initialization, race conditions could occur.
Consider adding a sync.RWMutex to protect concurrent access. This would make the pattern more robust and safer for future plugin extensibility.
| // ECSBackendFactory creates an ecsBackend for a given provider config. | ||
| type ECSBackendFactory func(cfg map[string]any) (ecsBackend, error) | ||
|
|
||
| // ecsBackendRegistry maps provider name to its factory. | ||
| var ecsBackendRegistry = map[string]ECSBackendFactory{} | ||
|
|
||
| // RegisterECSBackend registers an ECSBackendFactory for the given provider name. | ||
| func RegisterECSBackend(provider string, factory ECSBackendFactory) { | ||
| ecsBackendRegistry[provider] = factory | ||
| } |
There was a problem hiding this comment.
The global backend registries (ecsBackendRegistry, dnsBackendRegistry, autoscalingBackendRegistry, apigatewayBackendRegistry) are accessed without synchronization. While all current registrations happen in init() functions (which run serially), these registries lack synchronization protection. If external plugins or runtime code were to call these registration functions concurrently with module initialization, race conditions could occur.
Consider adding a sync.RWMutex to protect concurrent access. This would make the pattern more robust and safer for future plugin extensibility.
| // DNSBackendFactory creates a dnsBackend for a given provider config. | ||
| type DNSBackendFactory func(cfg map[string]any) (dnsBackend, error) | ||
|
|
||
| // dnsBackendRegistry maps provider name to its factory. | ||
| var dnsBackendRegistry = map[string]DNSBackendFactory{} | ||
|
|
||
| // RegisterDNSBackend registers a DNSBackendFactory for the given provider name. | ||
| func RegisterDNSBackend(provider string, factory DNSBackendFactory) { | ||
| dnsBackendRegistry[provider] = factory | ||
| } |
There was a problem hiding this comment.
The global backend registries (dnsBackendRegistry, autoscalingBackendRegistry, apigatewayBackendRegistry) are accessed without synchronization. While all current registrations happen in init() functions (which run serially), these registries lack synchronization protection. If external plugins or runtime code were to call these registration functions concurrently with module initialization, race conditions could occur.
Consider adding a sync.RWMutex to protect concurrent access. This would make the pattern more robust and safer for future plugin extensibility.
| // AutoscalingBackendFactory creates an autoscalingBackend for a given provider config. | ||
| type AutoscalingBackendFactory func(cfg map[string]any) (autoscalingBackend, error) | ||
|
|
||
| // autoscalingBackendRegistry maps provider name to its factory. | ||
| var autoscalingBackendRegistry = map[string]AutoscalingBackendFactory{} | ||
|
|
||
| // RegisterAutoscalingBackend registers an AutoscalingBackendFactory for the given provider name. | ||
| func RegisterAutoscalingBackend(provider string, factory AutoscalingBackendFactory) { | ||
| autoscalingBackendRegistry[provider] = factory | ||
| } |
There was a problem hiding this comment.
The global backend registries are accessed without synchronization. While all current registrations happen in init() functions (which run serially), these registries lack synchronization protection. If external plugins or runtime code were to call these registration functions concurrently with module initialization, race conditions could occur.
Consider adding a sync.RWMutex to protect concurrent access. This would make the pattern more robust and safer for future plugin extensibility.
| // APIGatewayBackendFactory creates an apigatewayBackend for a given provider config. | ||
| type APIGatewayBackendFactory func(cfg map[string]any) (apigatewayBackend, error) | ||
|
|
||
| // apigatewayBackendRegistry maps provider name to its factory. | ||
| var apigatewayBackendRegistry = map[string]APIGatewayBackendFactory{} | ||
|
|
||
| // RegisterAPIGatewayBackend registers an APIGatewayBackendFactory for the given provider name. | ||
| func RegisterAPIGatewayBackend(provider string, factory APIGatewayBackendFactory) { | ||
| apigatewayBackendRegistry[provider] = factory | ||
| } |
There was a problem hiding this comment.
The global backend registries are accessed without synchronization. While all current registrations happen in init() functions (which run serially), these registries lack synchronization protection. If external plugins or runtime code were to call these registration functions concurrently with module initialization, race conditions could occur.
Consider adding a sync.RWMutex to protect concurrent access. This would make the pattern more robust and safer for future plugin extensibility.
Summary
CloudCredentialResolverregistry pattern in the cloud module, decoupling provider-specific credential logic from the core cloud account moduleTest plan
🤖 Generated with Claude Code