Skip to content

Commit 0c5766f

Browse files
authored
Refactor Magician build to be more modular (#15505)
1 parent 581b00f commit 0c5766f

File tree

10 files changed

+353
-205
lines changed

10 files changed

+353
-205
lines changed

mmv1/api/custom_errors.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package api
2+
3+
import "fmt"
4+
5+
// ErrProductVersionNotFound is returned when a product doesn't exist
6+
// at the specified version or any lower version.
7+
type ErrProductVersionNotFound struct {
8+
ProductName string
9+
Version string
10+
}
11+
12+
// Error implements the error interface.
13+
func (e *ErrProductVersionNotFound) Error() string {
14+
return fmt.Sprintf("%s does not have a '%s' version", e.ProductName, e.Version)
15+
}

mmv1/api/product.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
package api
1515

1616
import (
17+
"errors"
1718
"fmt"
1819
"log"
20+
"os"
21+
"path/filepath"
1922
"reflect"
2023
"regexp"
24+
"sort"
2125
"strings"
2226
"unicode"
2327

@@ -34,6 +38,9 @@ type Product struct {
3438
// Example inputs: "Compute", "AccessContextManager"
3539
Name string
3640

41+
// This is the name of the package path relative to mmv1 root repo
42+
PackagePath string
43+
3744
// original value of :name before the provider override happens
3845
// same as :name if not overridden in provider
3946
ApiName string `yaml:"api_name,omitempty"`
@@ -77,6 +84,164 @@ type Product struct {
7784
Compiler string `yaml:"-"`
7885
}
7986

87+
// Load compiles a product with all its resources from the given path and optional overrides
88+
// This loads the product configuration and all its resources into memory without generating any files
89+
func (p *Product) Load(productName string, version string, overrideDirectory string) error {
90+
productYamlPath := filepath.Join(productName, "product.yaml")
91+
92+
var productOverridePath string
93+
if overrideDirectory != "" {
94+
productOverridePath = filepath.Join(overrideDirectory, productName, "product.yaml")
95+
}
96+
97+
_, baseProductErr := os.Stat(productYamlPath)
98+
baseProductExists := !errors.Is(baseProductErr, os.ErrNotExist)
99+
100+
_, overrideProductErr := os.Stat(productOverridePath)
101+
overrideProductExists := !errors.Is(overrideProductErr, os.ErrNotExist)
102+
103+
if !(baseProductExists || overrideProductExists) {
104+
return fmt.Errorf("%s does not contain a product.yaml file", productName)
105+
}
106+
107+
// Compile the product configuration
108+
if overrideProductExists {
109+
if baseProductExists {
110+
Compile(productYamlPath, p, overrideDirectory)
111+
overrideApiProduct := &Product{}
112+
Compile(productOverridePath, overrideApiProduct, overrideDirectory)
113+
Merge(reflect.ValueOf(p).Elem(), reflect.ValueOf(*overrideApiProduct), version)
114+
} else {
115+
Compile(productOverridePath, p, overrideDirectory)
116+
}
117+
} else {
118+
Compile(productYamlPath, p, overrideDirectory)
119+
}
120+
121+
// Check if product exists at the requested version
122+
if !p.ExistsAtVersionOrLower(version) {
123+
return &ErrProductVersionNotFound{ProductName: productName, Version: version}
124+
}
125+
126+
// Compile all resources
127+
resources, err := p.loadResources(productName, version, overrideDirectory)
128+
if err != nil {
129+
return err
130+
}
131+
132+
p.Objects = resources
133+
p.PackagePath = productName
134+
p.Validate()
135+
136+
return nil
137+
}
138+
139+
// loadResources loads all resources for a product
140+
func (p *Product) loadResources(productName string, version string, overrideDirectory string) ([]*Resource, error) {
141+
var resources []*Resource = make([]*Resource, 0)
142+
143+
// Get base resource files
144+
resourceFiles, err := filepath.Glob(fmt.Sprintf("%s/*", productName))
145+
if err != nil {
146+
return nil, fmt.Errorf("cannot get resource files: %v", err)
147+
}
148+
149+
// Compile base resources (skip those that will be merged with overrides)
150+
for _, resourceYamlPath := range resourceFiles {
151+
if filepath.Base(resourceYamlPath) == "product.yaml" || filepath.Ext(resourceYamlPath) != ".yaml" {
152+
continue
153+
}
154+
155+
// Skip if resource will be merged in the override loop
156+
if overrideDirectory != "" {
157+
resourceOverridePath := filepath.Join(overrideDirectory, resourceYamlPath)
158+
_, overrideResourceErr := os.Stat(resourceOverridePath)
159+
if !errors.Is(overrideResourceErr, os.ErrNotExist) {
160+
continue
161+
}
162+
}
163+
164+
resource := p.loadResource(resourceYamlPath, "", version, overrideDirectory)
165+
resources = append(resources, resource)
166+
}
167+
168+
// Compile override resources
169+
if overrideDirectory != "" {
170+
resources, err = p.reconcileOverrideResources(productName, version, overrideDirectory, resources)
171+
if err != nil {
172+
return nil, err
173+
}
174+
}
175+
176+
return resources, nil
177+
}
178+
179+
// reconcileOverrideResources handles resolution of override resources
180+
func (p *Product) reconcileOverrideResources(productName string, version string, overrideDirectory string, resources []*Resource) ([]*Resource, error) {
181+
productOverridePath := filepath.Join(overrideDirectory, productName, "product.yaml")
182+
productOverrideDir := filepath.Dir(productOverridePath)
183+
184+
overrideFiles, err := filepath.Glob(fmt.Sprintf("%s/*", productOverrideDir))
185+
if err != nil {
186+
return nil, fmt.Errorf("cannot get override files: %v", err)
187+
}
188+
189+
for _, overrideYamlPath := range overrideFiles {
190+
if filepath.Base(overrideYamlPath) == "product.yaml" || filepath.Ext(overrideYamlPath) != ".yaml" {
191+
continue
192+
}
193+
194+
baseResourcePath := filepath.Join(productName, filepath.Base(overrideYamlPath))
195+
resource := p.loadResource(baseResourcePath, overrideYamlPath, version, overrideDirectory)
196+
resources = append(resources, resource)
197+
}
198+
199+
// Sort resources by name for consistent output
200+
sort.Slice(resources, func(i, j int) bool {
201+
return resources[i].Name < resources[j].Name
202+
})
203+
204+
return resources, nil
205+
}
206+
207+
// loadResource loads a single resource with optional override
208+
func (p *Product) loadResource(baseResourcePath string, overrideResourcePath string, version string, overrideDirectory string) *Resource {
209+
resource := &Resource{}
210+
211+
// Check if base resource exists
212+
_, baseResourceErr := os.Stat(baseResourcePath)
213+
baseResourceExists := !errors.Is(baseResourceErr, os.ErrNotExist)
214+
215+
if overrideResourcePath != "" {
216+
if baseResourceExists {
217+
// Merge base and override
218+
Compile(baseResourcePath, resource, overrideDirectory)
219+
overrideResource := &Resource{}
220+
Compile(overrideResourcePath, overrideResource, overrideDirectory)
221+
Merge(reflect.ValueOf(resource).Elem(), reflect.ValueOf(*overrideResource), version)
222+
resource.SourceYamlFile = baseResourcePath
223+
} else {
224+
// Override only
225+
Compile(overrideResourcePath, resource, overrideDirectory)
226+
}
227+
} else {
228+
// Base only
229+
Compile(baseResourcePath, resource, overrideDirectory)
230+
resource.SourceYamlFile = baseResourcePath
231+
}
232+
233+
// Set resource defaults and validate
234+
resource.TargetVersionName = version
235+
// SetDefault before AddExtraFields to ensure relevant metadata is available on existing fields
236+
resource.SetDefault(p)
237+
resource.Properties = resource.AddExtraFields(resource.PropertiesWithExcluded(), nil)
238+
// SetDefault after AddExtraFields to ensure relevant metadata is available for the newly generated fields
239+
resource.SetDefault(p)
240+
resource.Validate()
241+
242+
return resource
243+
}
244+
80245
func (p *Product) UnmarshalYAML(unmarshal func(any) error) error {
81246
type productAlias Product
82247
aliasObj := (*productAlias)(p)

mmv1/loader/loader.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package loader
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"sync"
10+
11+
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api"
12+
"golang.org/x/exp/slices"
13+
)
14+
15+
type Loader struct {
16+
OverrideDirectory string
17+
Version string
18+
}
19+
20+
func (l *Loader) LoadProducts() map[string]*api.Product {
21+
if l.Version == "" {
22+
log.Printf("No version specified, assuming ga")
23+
l.Version = "ga"
24+
}
25+
26+
var allProductFiles []string = make([]string, 0)
27+
28+
files, err := filepath.Glob("products/**/product.yaml")
29+
if err != nil {
30+
panic(err)
31+
}
32+
for _, filePath := range files {
33+
dir := filepath.Dir(filePath)
34+
allProductFiles = append(allProductFiles, fmt.Sprintf("products/%s", filepath.Base(dir)))
35+
}
36+
37+
if l.OverrideDirectory != "" {
38+
log.Printf("Using override directory %s", l.OverrideDirectory)
39+
40+
// Normalize override dir to a path that is relative to the magic-modules directory
41+
// This is needed for templates that concatenate pwd + override dir + path
42+
if filepath.IsAbs(l.OverrideDirectory) {
43+
wd, err := os.Getwd()
44+
if err != nil {
45+
panic(err)
46+
}
47+
l.OverrideDirectory, err = filepath.Rel(wd, l.OverrideDirectory)
48+
log.Printf("Override directory normalized to relative path %s", l.OverrideDirectory)
49+
}
50+
51+
overrideFiles, err := filepath.Glob(fmt.Sprintf("%s/products/**/product.yaml", l.OverrideDirectory))
52+
if err != nil {
53+
panic(err)
54+
}
55+
for _, filePath := range overrideFiles {
56+
product, err := filepath.Rel(l.OverrideDirectory, filePath)
57+
if err != nil {
58+
panic(err)
59+
}
60+
dir := filepath.Dir(product)
61+
productFile := fmt.Sprintf("products/%s", filepath.Base(dir))
62+
if !slices.Contains(allProductFiles, productFile) {
63+
allProductFiles = append(allProductFiles, productFile)
64+
}
65+
}
66+
}
67+
68+
return l.batchLoadProducts(allProductFiles)
69+
}
70+
71+
func (l *Loader) batchLoadProducts(productNames []string) map[string]*api.Product {
72+
products := make(map[string]*api.Product)
73+
74+
// Create result type for clarity
75+
type loadResult struct {
76+
name string
77+
product *api.Product
78+
err error
79+
}
80+
81+
// Buffered channel to prevent goroutine blocking
82+
productChan := make(chan loadResult, len(productNames))
83+
84+
// Use WaitGroup for proper synchronization
85+
var wg sync.WaitGroup
86+
87+
// Launch all goroutines
88+
for _, productName := range productNames {
89+
wg.Add(1)
90+
go func(name string) {
91+
defer wg.Done()
92+
93+
product, err := l.loadProductOnly(name)
94+
productChan <- loadResult{
95+
name: name,
96+
product: product,
97+
err: err,
98+
}
99+
}(productName)
100+
}
101+
102+
wg.Wait()
103+
close(productChan)
104+
105+
// Collect results as they complete
106+
loadFailureCount := 0
107+
for result := range productChan {
108+
if result.err != nil {
109+
// Check if the error is the specific "version not found" error
110+
var versionErr *api.ErrProductVersionNotFound
111+
if errors.As(result.err, &versionErr) {
112+
continue
113+
}
114+
115+
loadFailureCount++
116+
log.Printf("Error loading %s: %v", result.name, result.err)
117+
continue
118+
}
119+
products[result.name] = result.product
120+
}
121+
if loadFailureCount > 0 {
122+
log.Fatalf("Failed to load %d products", loadFailureCount)
123+
}
124+
125+
return products
126+
}
127+
128+
// loadProductOnly is a standalone function to just load a product without generation
129+
// This can be used when you only need to load and validate a product configuration
130+
func (l *Loader) loadProductOnly(productName string) (*api.Product, error) {
131+
product := &api.Product{}
132+
err := product.Load(productName, l.Version, l.OverrideDirectory)
133+
if err != nil {
134+
return nil, err
135+
}
136+
return product, nil
137+
}

0 commit comments

Comments
 (0)