Skip to content

refactor: introduce provider registries for cloud and platform decoupling#174

Merged
intel352 merged 2 commits intomainfrom
fix/ws04-cloud-provider-decoupling
Feb 26, 2026
Merged

refactor: introduce provider registries for cloud and platform decoupling#174
intel352 merged 2 commits intomainfrom
fix/ws04-cloud-provider-decoupling

Conversation

@intel352
Copy link
Contributor

Summary

  • Introduce CloudCredentialResolver registry pattern in the cloud module, decoupling provider-specific credential logic from the core cloud account module
  • Introduce backend factory registries for platform modules, allowing platform backends (EKS, GKE, AKS, etc.) to be registered and looked up dynamically without hard-coded switch statements
  • Reduces coupling between the core engine and cloud provider implementations, enabling external plugins to register their own cloud credential resolvers and platform backends

Test plan

  • go build ./...
  • go test ./...

🤖 Generated with Claude Code

intel352 and others added 2 commits February 26, 2026 16:08
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>
Copilot AI review requested due to automatic review settings February 26, 2026 21:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CloudCredentialResolver interface 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.go into 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

Comment on lines +12 to +74
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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +24
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +76
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +63
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +86
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +64
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +58
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +67
// 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
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@intel352 intel352 merged commit 5149b59 into main Feb 26, 2026
18 checks passed
@intel352 intel352 deleted the fix/ws04-cloud-provider-decoupling branch February 26, 2026 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants