Skip to content

Commit 7b3c803

Browse files
authored
Made meta.yaml tests raise parsing errors and enforce accuracy (#16102)
1 parent ccf972c commit 7b3c803

File tree

5 files changed

+400
-255
lines changed

5 files changed

+400
-255
lines changed

mmv1/third_party/terraform/acctest/resource_inventory_reader.go.tmpl

Lines changed: 106 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,181 +4,154 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"slices"
78
"strings"
89
"sync"
910

1011
"gopkg.in/yaml.v2"
1112
)
1213

13-
// ResourceYAMLMetadata represents the structure of the metadata files
14-
type ResourceYAMLMetadata struct {
15-
Resource string `yaml:"resource"`
16-
ApiServiceName string `yaml:"api_service_name"`
17-
CaiAssetNameFormat string `yaml:"cai_asset_name_format"`
18-
SourceFile string `yaml:"source_file"`
19-
}
20-
21-
// Cache structures to avoid repeated file system operations
2214
var (
23-
// Cache for API service names (resourceName -> apiServiceName)
24-
ApiServiceNameCache = NewGenericCache("unknown")
25-
// Cache for CAI resource name format (resourceName -> CaiAssetNameFormat)
26-
CaiAssetNameFormatCache = NewGenericCache("")
27-
// Cache for service packages (resourceType -> servicePackage)
28-
ServicePackageCache = NewGenericCache("unknown")
29-
// Flag to track if cache has been populated
30-
cachePopulated = false
31-
// Mutex to protect cache access
32-
cacheMutex sync.RWMutex
33-
34-
iamSuffixes = []string{
35-
"_iam_member",
36-
"_iam_binding",
37-
"_iam_policy",
15+
// The GlobalMetadataCache is used by VCR tests to avoid loading metadata once per test run.
16+
// Because of the way VCR tests are run, it's difficult to avoid a global variable.
17+
GlobalMetadataCache = MetadataCache{
18+
mutex: &sync.Mutex{},
3819
}
39-
4020
{{ if eq $.TargetVersionName `ga` -}}
4121
providerName = "google"
4222
{{- else }}
4323
providerName = "google-beta"
4424
{{- end }}
45-
4625
)
4726

48-
// PopulateMetadataCache walks through all metadata files once and populates
49-
// both the API service name and service package caches for improved performance
50-
func PopulateMetadataCache() error {
51-
cacheMutex.Lock()
52-
defer cacheMutex.Unlock()
27+
// Metadata represents the structure of the metadata files
28+
type Metadata struct {
29+
Resource string `yaml:"resource"`
30+
GenerationType string `yaml:"generation_type"`
31+
SourceFile string `yaml:"source_file"`
32+
ApiServiceName string `yaml:"api_service_name"`
33+
ApiVersion string `yaml:"api_version"`
34+
ApiResourceTypeKind string `yaml:"api_resource_type_kind"`
35+
CaiAssetNameFormat string `yaml:"cai_asset_name_format"`
36+
ApiVariantPatterns []string `yaml:"api_variant_patterns"`
37+
AutogenStatus bool `yaml:"autogen_status,omitempty"`
38+
Fields []MetadataField `yaml:"fields"`
39+
40+
// These keys store information about the metadata file itself.
41+
42+
// Path is the path of the loaded metadata file
43+
Path string
44+
// ServicePackage is the folder within services/ that the metadata file is in
45+
ServicePackage string
46+
}
5347

54-
// If cache is already populated, we can skip
55-
if cachePopulated {
56-
return nil
48+
type MetadataField struct {
49+
ApiField string `yaml:"api_field"`
50+
Field string `yaml:"field"`
51+
ProviderOnly string `yaml:"provider_only"`
52+
Json string `yaml:"json"`
53+
}
54+
55+
type MetadataCache struct {
56+
mutex *sync.Mutex
57+
cache map[string]Metadata
58+
populated bool
59+
populatedError error
60+
}
61+
62+
func (mc *MetadataCache) Populate() error {
63+
mc.mutex.Lock()
64+
defer mc.mutex.Unlock()
65+
66+
if mc.populated {
67+
return mc.populatedError
5768
}
5869

5970
baseDir, err := getServicesDir()
6071
if err != nil {
6172
return fmt.Errorf("failed to find services directory: %v", err)
6273
}
6374

64-
// Count for statistics
65-
apiNameCount := 0
66-
servicePkgCount := 0
75+
mc.cache = make(map[string]Metadata)
76+
77+
var malformed_yaml_errs []string
6778

6879
// Walk through all service directories once
6980
err = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
7081
if err != nil {
71-
return nil // Skip files with errors but continue walking
82+
return err // Fail immediately if there's an OS error.
83+
}
84+
85+
// Skip non-metadata files
86+
if info.IsDir() || !strings.HasPrefix(info.Name(), "resource_") || !strings.HasSuffix(info.Name(), "_meta.yaml") {
87+
return nil
88+
}
89+
90+
// Read the file
91+
content, err := os.ReadFile(path)
92+
if err != nil {
93+
return err // Fail immediately if there's an OS error.
7294
}
7395

74-
// Look for metadata files
75-
if !info.IsDir() && strings.HasPrefix(info.Name(), "resource_") && strings.HasSuffix(info.Name(), "_meta.yaml") {
76-
// Read the file
77-
content, err := os.ReadFile(path)
78-
if err != nil {
79-
return nil // Continue to next file
80-
}
81-
82-
// Parse YAML
83-
var metadata ResourceYAMLMetadata
84-
if err := yaml.Unmarshal(content, &metadata); err != nil {
85-
return nil // Continue to next file
86-
}
87-
88-
// Skip if resource is empty
89-
if metadata.Resource == "" {
90-
return nil
91-
}
92-
93-
iamResources := make([]string, 0)
94-
for _, suffix := range iamSuffixes {
95-
iamResources = append(iamResources, fmt.Sprintf("%s%s", metadata.Resource, suffix))
96-
}
97-
98-
// Store API service name in cache
99-
if metadata.ApiServiceName != "" {
100-
ApiServiceNameCache.Set(metadata.Resource, metadata.ApiServiceName)
101-
for _, iamResource := range iamResources {
102-
ApiServiceNameCache.Set(iamResource, metadata.ApiServiceName)
103-
}
104-
apiNameCount++
105-
}
106-
107-
if metadata.CaiAssetNameFormat != "" {
108-
CaiAssetNameFormatCache.Set(metadata.Resource, metadata.CaiAssetNameFormat)
109-
for _, iamResource := range iamResources {
110-
CaiAssetNameFormatCache.Set(iamResource, metadata.CaiAssetNameFormat)
111-
}
112-
}
113-
114-
// Extract and store service package in cache
115-
pathParts := strings.Split(path, string(os.PathSeparator))
116-
servicesIndex := -1
117-
for i, part := range pathParts {
118-
if part == "services" {
119-
servicesIndex = i
120-
break
121-
}
122-
}
123-
124-
if servicesIndex >= 0 && len(pathParts) > servicesIndex+1 {
125-
servicePackage := pathParts[servicesIndex+1] // The part after "services"
126-
ServicePackageCache.Set(metadata.Resource, servicePackage)
127-
for _, iamResource := range iamResources {
128-
ServicePackageCache.Set(iamResource, servicePackage)
129-
}
130-
servicePkgCount++
131-
}
96+
// Parse YAML
97+
var metadata Metadata
98+
if err := yaml.Unmarshal(content, &metadata); err != nil {
99+
// note but keep walking
100+
malformed_yaml_errs = append(malformed_yaml_errs, fmt.Sprintf("%s: %v", path, err.Error()))
101+
return nil
132102
}
103+
104+
// Skip if resource is empty
105+
if metadata.Resource == "" {
106+
return nil
107+
}
108+
109+
if _, ok := mc.cache[metadata.Resource]; ok {
110+
return fmt.Errorf("duplicate resource: %s in %s", metadata.Resource, path)
111+
}
112+
113+
metadata.Path = path
114+
pathParts := strings.Split(path, string(os.PathSeparator))
115+
servicesIndex := slices.Index(pathParts, "services")
116+
if servicesIndex == -1 {
117+
return fmt.Errorf("no service found for %s (%s)", metadata.Resource, path)
118+
}
119+
metadata.ServicePackage = pathParts[servicesIndex+1]
120+
mc.cache[metadata.Resource] = metadata
133121
return nil
134122
})
135123

136124
if err != nil {
137-
return fmt.Errorf("error walking directory: %v", err)
125+
mc.populatedError = fmt.Errorf("error walking directory: %v", err)
138126
}
139127

140-
// Mark cache as populated
141-
cachePopulated = true
142-
143-
return nil
144-
}
128+
if len(malformed_yaml_errs) > 0 {
129+
mc.populatedError = fmt.Errorf("YAML parsing errors encountered:\n%v", strings.Join(malformed_yaml_errs, "\n"))
130+
}
145131

146-
type GenericCache struct {
147-
mu sync.RWMutex
148-
data map[string]string
149-
defaultValue string
150-
}
132+
// Mark cache as populated
133+
mc.populated = true
151134

152-
// NewGenericCache initializes a new GenericCache with a default value.
153-
func NewGenericCache(defaultValue string) *GenericCache {
154-
return &GenericCache{
155-
data: make(map[string]string),
156-
defaultValue: defaultValue,
157-
}
135+
return mc.populatedError
158136
}
159137

160-
// Get retrieves a value from the cache, returning the default if not found.
161-
func (c *GenericCache) Get(key string) string {
162-
// Make sure cache is populated
163-
if !cachePopulated {
164-
if err := PopulateMetadataCache(); err != nil {
165-
return "failed_to_populate_metadata_cache"
166-
}
167-
}
168-
169-
c.mu.RLock()
170-
defer c.mu.RUnlock()
171-
value, ok := c.data[key]
172-
if !ok {
173-
return c.defaultValue
174-
}
175-
return value
138+
// Get takes a resource name (like google_compute_instance) and returns
139+
// the metadata for that resource and an `ok` bool of whether the metadata
140+
// exisxts in the cache.
141+
func (mc *MetadataCache) Get(key string) (Metadata, bool) {
142+
// For IAM resources, return the parent resource's metadata. This could change if we
143+
// start generating separate IAM metadata in the future. This is primarily for
144+
// backwards-compatibility with the previous cache behavior and a workaround for _not_
145+
// generating IAM resource metadata.
146+
key, _ = strings.CutSuffix(key, "_iam_member")
147+
key, _ = strings.CutSuffix(key, "_iam_binding")
148+
key, _ = strings.CutSuffix(key, "_iam_policy")
149+
m, ok := mc.cache[key]
150+
return m, ok
176151
}
177152

178-
func (c *GenericCache) Set(key, value string) {
179-
c.mu.Lock()
180-
defer c.mu.Unlock()
181-
c.data[key] = value
153+
func (mc *MetadataCache) Cache() map[string]Metadata {
154+
return mc.cache
182155
}
183156

184157
// getServicesDir returns the path to the services directory

0 commit comments

Comments
 (0)