@@ -24,6 +24,7 @@ import (
24
24
"path/filepath"
25
25
"runtime"
26
26
"strings"
27
+ "time"
27
28
28
29
"github.com/spf13/cobra"
29
30
@@ -39,6 +40,71 @@ import (
39
40
concpool "github.com/sourcegraph/conc/pool"
40
41
)
41
42
43
+ // PackageMetadataProvider is an interface for providers that retrieve package metadata
44
+ // from either the filesystem directory or the Pulumi Registry API.
45
+ type PackageMetadataProvider interface {
46
+ // GetPackageMetadata returns metadata for a specific package
47
+ GetPackageMetadata (pkgName string ) (* pkg.PackageMeta , error )
48
+ // ListPackageMetadata returns metadata for all packages
49
+ ListPackageMetadata () ([]* pkg.PackageMeta , error )
50
+ }
51
+
52
+ // FileSystemProvider implements PackageMetadataProvider using the local yaml data files
53
+ // in the pulumi/registry repository.
54
+ type FileSystemProvider struct {
55
+ registryDir string
56
+ }
57
+
58
+ // RegistryAPIProvider implements PackageMetadataProvider using the Pulumi API
59
+ // to retrieve package metadata.
60
+ type RegistryAPIProvider struct {
61
+ apiURL string
62
+ }
63
+
64
+ // PackageMetadata represents the API response structure for package metadata
65
+ // from the Pulumi Registry API.
66
+ // TODO: import type from pulumi-service if possible
67
+ type PackageMetadata struct {
68
+ Name string `json:"name"`
69
+ Publisher string `json:"publisher"`
70
+ Source string `json:"source"`
71
+ Version string `json:"version"`
72
+ Title string `json:"title,omitempty"`
73
+ Description string `json:"description,omitempty"`
74
+ LogoURL string `json:"logoUrl,omitempty"`
75
+ RepoURL string `json:"repoUrl,omitempty"`
76
+ Category string `json:"category,omitempty"`
77
+ IsFeatured bool `json:"isFeatured"`
78
+ PackageTypes []string `json:"packageTypes,omitempty"`
79
+ PackageStatus string `json:"packageStatus"`
80
+ SchemaURL string `json:"schemaURL"`
81
+ CreatedAt time.Time `json:"createdAt"`
82
+ }
83
+
84
+ // NewFileSystemProvider creates a new FileSystemProvider
85
+ func NewFileSystemProvider (registryDir string ) * FileSystemProvider {
86
+ return & FileSystemProvider {
87
+ registryDir : registryDir ,
88
+ }
89
+ }
90
+
91
+ // NewAPIProvider creates a new RegistryAPIProvider
92
+ func NewAPIProvider (apiURL string ) * RegistryAPIProvider {
93
+ return & RegistryAPIProvider {
94
+ apiURL : apiURL ,
95
+ }
96
+ }
97
+
98
+ // contains checks if a string is in a slice
99
+ func contains (slice []string , item string ) bool {
100
+ for _ , s := range slice {
101
+ if s == item {
102
+ return true
103
+ }
104
+ }
105
+ return false
106
+ }
107
+
42
108
func getRepoSlug (repoURL string ) (string , error ) {
43
109
u , err := url .Parse (repoURL )
44
110
if err != nil {
@@ -136,85 +202,87 @@ func getRegistryPackagesPath(repoPath string) string {
136
202
return filepath .Join (repoPath , "themes" , "default" , "data" , "registry" , "packages" )
137
203
}
138
204
139
- func genResourceDocsForAllRegistryPackages (registryRepoPath , baseDocsOutDir , basePackageTreeJSONOutDir string ) error {
140
- registryPackagesPath := getRegistryPackagesPath (registryRepoPath )
141
- metadataFiles , err := os .ReadDir (registryPackagesPath )
205
+ func genResourceDocsForAllRegistryPackages (
206
+ provider PackageMetadataProvider ,
207
+ baseDocsOutDir , basePackageTreeJSONOutDir string ,
208
+ ) error {
209
+ metadataList , err := provider .ListPackageMetadata ()
142
210
if err != nil {
143
- return errors .Wrap (err , "reading the registry packages dir " )
211
+ return errors .Wrap (err , "listing package metadata " )
144
212
}
145
213
146
214
pool := concpool .New ().WithErrors ().WithMaxGoroutines (runtime .NumCPU ())
147
- for _ , f := range metadataFiles {
148
- f := f
215
+ for _ , metadata := range metadataList {
216
+ metadata := metadata
149
217
pool .Go (func () error {
150
- glog .Infof ("=== starting %s ===\n " , f .Name ())
151
- glog .Infoln ("Processing metadata file" )
152
- metadataFilePath := filepath .Join (registryPackagesPath , f .Name ())
153
-
154
- b , err := os .ReadFile (metadataFilePath )
155
- if err != nil {
156
- return errors .Wrapf (err , "reading the metadata file %s" , metadataFilePath )
157
- }
158
-
159
- var metadata pkg.PackageMeta
160
- if err := yaml .Unmarshal (b , & metadata ); err != nil {
161
- return errors .Wrapf (err , "unmarshalling the metadata file %s" , metadataFilePath )
162
- }
163
-
218
+ glog .Infof ("=== starting %s ===\n " , metadata .Name )
164
219
docsOutDir := filepath .Join (baseDocsOutDir , metadata .Name , "api-docs" )
165
- err = genResourceDocsForPackageFromRegistryMetadata (metadata , docsOutDir , basePackageTreeJSONOutDir )
220
+ err = genResourceDocsForPackageFromRegistryMetadata (* metadata , docsOutDir , basePackageTreeJSONOutDir )
166
221
if err != nil {
167
- return errors .Wrapf (err , "generating resource docs using metadata file info %s" , f .Name () )
222
+ return errors .Wrapf (err , "generating resource docs using metadata file info %s" , metadata .Name )
168
223
}
169
224
170
- glog .Infof ("=== completed %s ===" , f .Name () )
225
+ glog .Infof ("=== completed %s ===" , metadata .Name )
171
226
return nil
172
227
})
173
228
}
174
-
175
229
return pool .Wait ()
176
230
}
177
231
232
+ func convertAPIPackageToPackageMeta (apiPkg PackageMetadata ) (* pkg.PackageMeta , error ) {
233
+ return & pkg.PackageMeta {
234
+ Name : apiPkg .Name ,
235
+ Publisher : apiPkg .Publisher ,
236
+ Description : apiPkg .Description ,
237
+ LogoURL : apiPkg .LogoURL ,
238
+ RepoURL : apiPkg .RepoURL ,
239
+ Category : pkg .PackageCategory (apiPkg .Category ),
240
+ Featured : apiPkg .IsFeatured ,
241
+ Native : contains (apiPkg .PackageTypes , "native" ),
242
+ Component : contains (apiPkg .PackageTypes , "component" ),
243
+ PackageStatus : pkg .PackageStatus (apiPkg .PackageStatus ),
244
+ SchemaFileURL : apiPkg .SchemaURL ,
245
+ Version : apiPkg .Version ,
246
+ Title : apiPkg .Title ,
247
+ UpdatedOn : apiPkg .CreatedAt .Unix (),
248
+ }, nil
249
+ }
250
+
178
251
func resourceDocsFromRegistryCmd () * cobra.Command {
179
252
var baseDocsOutDir string
180
253
var basePackageTreeJSONOutDir string
181
254
var registryDir string
255
+ var useAPI bool
256
+ var apiURL string
182
257
183
258
cmd := & cobra.Command {
184
259
Use : "registry [pkgName]" ,
185
260
Short : "Generate resource docs for a package from the registry" ,
186
261
Long : "Generate resource docs for all packages in the registry or specific packages. " +
187
262
"Pass a package name in the registry as an optional arg to generate docs only for that package." ,
188
263
RunE : func (cmd * cobra.Command , args []string ) error {
189
- registryDir , err := filepath .Abs (registryDir )
190
- if err != nil {
191
- return errors .Wrap (err , "finding the cwd" )
264
+ var provider PackageMetadataProvider
265
+ if useAPI {
266
+ provider = NewAPIProvider (apiURL )
267
+ } else {
268
+ provider = NewFileSystemProvider (registryDir )
192
269
}
193
270
194
271
if len (args ) > 0 {
195
272
glog .Infoln ("Generating docs for a single package:" , args [0 ])
196
- registryPackagesPath := getRegistryPackagesPath (registryDir )
197
- pkgName := args [0 ]
198
- metadataFilePath := filepath .Join (registryPackagesPath , pkgName + ".yaml" )
199
- b , err := os .ReadFile (metadataFilePath )
273
+ metadata , err := provider .GetPackageMetadata (args [0 ])
200
274
if err != nil {
201
- return errors .Wrapf (err , "reading the metadata file %s" , metadataFilePath )
202
- }
203
-
204
- var metadata pkg.PackageMeta
205
- if err := yaml .Unmarshal (b , & metadata ); err != nil {
206
- return errors .Wrapf (err , "unmarshalling the metadata file %s" , metadataFilePath )
275
+ return errors .Wrapf (err , "getting metadata for package %q" , args [0 ])
207
276
}
208
277
209
278
docsOutDir := filepath .Join (baseDocsOutDir , metadata .Name , "api-docs" )
210
-
211
- err = genResourceDocsForPackageFromRegistryMetadata (metadata , docsOutDir , basePackageTreeJSONOutDir )
279
+ err = genResourceDocsForPackageFromRegistryMetadata (* metadata , docsOutDir , basePackageTreeJSONOutDir )
212
280
if err != nil {
213
- return errors .Wrapf (err , "generating docs for package %q from registry metadata" , pkgName )
281
+ return errors .Wrapf (err , "generating docs for package %q from registry metadata" , args [ 0 ] )
214
282
}
215
283
} else {
216
284
glog .Infoln ("Generating docs for all packages in the registry..." )
217
- err := genResourceDocsForAllRegistryPackages (registryDir , baseDocsOutDir , basePackageTreeJSONOutDir )
285
+ err := genResourceDocsForAllRegistryPackages (provider , baseDocsOutDir , basePackageTreeJSONOutDir )
218
286
if err != nil {
219
287
return errors .Wrap (err , "generating docs for all packages from registry metadata" )
220
288
}
@@ -234,6 +302,109 @@ func resourceDocsFromRegistryCmd() *cobra.Command {
234
302
cmd .Flags ().StringVar (& registryDir , "registryDir" ,
235
303
"." ,
236
304
"The root of the pulumi/registry directory" )
305
+ cmd .Flags ().BoolVar (& useAPI , "use-api" , false , "Use the Pulumi Registry API instead of local files" )
306
+ cmd .Flags ().StringVar (& apiURL , "api-url" ,
307
+ "https://api.pulumi.com/api/preview/registry" ,
308
+ "URL of the Pulumi Registry API" )
237
309
238
310
return cmd
239
311
}
312
+
313
+ // GetPackageMetadata implements PackageMetadataProvider for FileSystemProvider
314
+ func (p * FileSystemProvider ) GetPackageMetadata (pkgName string ) (* pkg.PackageMeta , error ) {
315
+ metadataFilePath := filepath .Join (getRegistryPackagesPath (p .registryDir ), pkgName + ".yaml" )
316
+ b , err := os .ReadFile (metadataFilePath )
317
+ if err != nil {
318
+ return nil , errors .Wrapf (err , "reading the metadata file %s" , metadataFilePath )
319
+ }
320
+
321
+ var metadata pkg.PackageMeta
322
+ if err := yaml .Unmarshal (b , & metadata ); err != nil {
323
+ return nil , errors .Wrapf (err , "unmarshalling the metadata file %s" , metadataFilePath )
324
+ }
325
+
326
+ return & metadata , nil
327
+ }
328
+
329
+ // ListPackageMetadata implements PackageMetadataProvider for FileSystemProvider
330
+ func (p * FileSystemProvider ) ListPackageMetadata () ([]* pkg.PackageMeta , error ) {
331
+ registryPackagesPath := getRegistryPackagesPath (p .registryDir )
332
+ files , err := os .ReadDir (registryPackagesPath )
333
+ if err != nil {
334
+ return nil , errors .Wrapf (err , "reading directory %s" , registryPackagesPath )
335
+ }
336
+
337
+ // Count YAML files to pre-allocate the slice mostly to appease the linter.
338
+ var yamlCount int
339
+ for _ , file := range files {
340
+ if strings .HasSuffix (file .Name (), ".yaml" ) {
341
+ yamlCount ++
342
+ }
343
+ }
344
+
345
+ metadataList := make ([]* pkg.PackageMeta , 0 , yamlCount )
346
+ for _ , file := range files {
347
+ if ! strings .HasSuffix (file .Name (), ".yaml" ) {
348
+ continue
349
+ }
350
+
351
+ metadata , err := p .GetPackageMetadata (strings .TrimSuffix (file .Name (), ".yaml" ))
352
+ if err != nil {
353
+ return nil , err
354
+ }
355
+ metadataList = append (metadataList , metadata )
356
+ }
357
+
358
+ return metadataList , nil
359
+ }
360
+
361
+ // GetPackageMetadata implements PackageMetadataProvider for RegistryAPIProvider
362
+ func (p * RegistryAPIProvider ) GetPackageMetadata (pkgName string ) (* pkg.PackageMeta , error ) {
363
+ resp , err := http .Get (fmt .Sprintf ("%s/packages?name=%s" , p .apiURL , pkgName ))
364
+ if err != nil {
365
+ return nil , errors .Wrapf (err , "fetching package metadata from API for %s" , pkgName )
366
+ }
367
+ defer resp .Body .Close ()
368
+
369
+ if resp .StatusCode != http .StatusOK {
370
+ return nil , errors .Errorf ("unexpected status code %d when fetching package metadata" , resp .StatusCode )
371
+ }
372
+
373
+ var apiPkg PackageMetadata
374
+ if err := json .NewDecoder (resp .Body ).Decode (& apiPkg ); err != nil {
375
+ return nil , errors .Wrap (err , "decoding API response" )
376
+ }
377
+
378
+ return convertAPIPackageToPackageMeta (apiPkg )
379
+ }
380
+
381
+ // ListPackageMetadata implements PackageMetadataProvider for RegistryAPIProvider
382
+ func (p * RegistryAPIProvider ) ListPackageMetadata () ([]* pkg.PackageMeta , error ) {
383
+ resp , err := http .Get (p .apiURL + "/packages" )
384
+ if err != nil {
385
+ return nil , errors .Wrap (err , "fetching package list from API" )
386
+ }
387
+ defer resp .Body .Close ()
388
+
389
+ if resp .StatusCode != http .StatusOK {
390
+ return nil , errors .Errorf ("unexpected status code %d when fetching package list" , resp .StatusCode )
391
+ }
392
+
393
+ var response struct {
394
+ Packages []PackageMetadata `json:"packages"`
395
+ }
396
+ if err := json .NewDecoder (resp .Body ).Decode (& response ); err != nil {
397
+ return nil , errors .Wrap (err , "decoding API response" )
398
+ }
399
+
400
+ metadataList := make ([]* pkg.PackageMeta , 0 , len (response .Packages ))
401
+ for _ , apiPkg := range response .Packages {
402
+ metadata , err := convertAPIPackageToPackageMeta (apiPkg )
403
+ if err != nil {
404
+ return nil , err
405
+ }
406
+ metadataList = append (metadataList , metadata )
407
+ }
408
+
409
+ return metadataList , nil
410
+ }
0 commit comments