@@ -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
2214var (
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