-
Notifications
You must be signed in to change notification settings - Fork 259
Description
Problem
The project package has tight coupling to the provisioning package through direct type usage in ProjectConfig:
// pkg/project/project_config.go
type ProjectConfig struct {
Infra provisioning.Options `yaml:"infra,omitempty"`
// ... other fields
}This creates a hard dependency where:
- The
projectpackage cannot be compiled without theprovisioningpackage - Parsing
azure.yamlrequires bringing in all provisioning infrastructure logic - Violates separation of concerns (config data vs. provisioning behavior)
- Makes isolated testing difficult
- Increases dependency bloat for consumers who only need config parsing
Current Issues
- Unnecessary dependencies: Importing
projectto parseazure.yamltransitively importsprovisioningand all its dependencies - Mixed concerns: Configuration parsing (data) is coupled with provisioning logic (behavior)
- Limited reusability: Cannot use
projectpackage independently for read-only config scenarios - Testing complexity: Testing config parsing requires provisioning infrastructure
Proposed Solution
Introduce a local InfraConfig type in the project package with conversion functions:
// pkg/project/infra_config.go
package project
// InfraConfig represents infrastructure configuration from azure.yaml
// This is a data-only representation that can be converted to provisioning.Options
type InfraConfig struct {
Provider string `yaml:"provider,omitempty"`
Path string `yaml:"path,omitempty"`
Module string `yaml:"module,omitempty"`
}
// ToProvisioningOptions converts InfraConfig to provisioning.Options
func (ic *InfraConfig) ToProvisioningOptions() provisioning.Options {
return provisioning.Options{
Provider: provisioning.ProviderKind(ic.Provider),
Path: ic.Path,
Module: ic.Module,
}
}
// FromProvisioningOptions creates InfraConfig from provisioning.Options
func InfraConfigFromProvisioningOptions(opts provisioning.Options) InfraConfig {
return InfraConfig{
Provider: string(opts.Provider),
Path: opts.Path,
Module: opts.Module,
}
}// pkg/project/project_config.go
type ProjectConfig struct {
Infra InfraConfig `yaml:"infra,omitempty"`
// ... other fields
}Benefits
✅ Loose coupling: project package becomes self-contained
✅ Independent usability: Parse azure.yaml without provisioning dependencies
✅ Separation of concerns: Clear boundary between data and behavior
✅ Better testability: Test config parsing in isolation
✅ Smaller dependency graph: Reduced transitive dependencies
✅ Type safety maintained: Compile-time validation with conversion functions
Migration Path
- Create
InfraConfigtype inprojectpackage - Update
ProjectConfigto useInfraConfig - Add conversion functions for backward compatibility
- Update call sites to convert when provisioning is needed:
// Before provisioningOpts := projectConfig.Infra // After provisioningOpts := projectConfig.Infra.ToProvisioningOptions()
- Update tests and verify YAML serialization remains unchanged
Architectural Principles
This refactoring follows:
- Dependency Inversion Principle: Depend on abstractions (data structures) not concretions (behavior)
- Single Responsibility Principle:
projecthandles config,provisioninghandles infrastructure - Separation of Concerns: Data vs. logic separation
- Loose Coupling: Packages can evolve independently
Similar Improvements
Consider applying this pattern to other tightly coupled fields in ProjectConfig:
State *state.ConfigPlatform *platform.ConfigCloud *cloud.ConfigWorkflows workflow.WorkflowMap
Each should be evaluated based on whether the coupling is necessary or can be reduced.
References
- File:
pkg/project/project_config.go:35 - Related: Package dependency analysis for
project→provisioningcoupling