@@ -24,6 +24,7 @@ import (
2424 "path/filepath"
2525 "runtime"
2626 "strings"
27+ "time"
2728
2829 "github.com/spf13/cobra"
2930
@@ -39,6 +40,71 @@ import (
3940 concpool "github.com/sourcegraph/conc/pool"
4041)
4142
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+
42108func getRepoSlug (repoURL string ) (string , error ) {
43109 u , err := url .Parse (repoURL )
44110 if err != nil {
@@ -136,85 +202,87 @@ func getRegistryPackagesPath(repoPath string) string {
136202 return filepath .Join (repoPath , "themes" , "default" , "data" , "registry" , "packages" )
137203}
138204
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 ()
142210 if err != nil {
143- return errors .Wrap (err , "reading the registry packages dir " )
211+ return errors .Wrap (err , "listing package metadata " )
144212 }
145213
146214 pool := concpool .New ().WithErrors ().WithMaxGoroutines (runtime .NumCPU ())
147- for _ , f := range metadataFiles {
148- f := f
215+ for _ , metadata := range metadataList {
216+ metadata := metadata
149217 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 )
164219 docsOutDir := filepath .Join (baseDocsOutDir , metadata .Name , "api-docs" )
165- err = genResourceDocsForPackageFromRegistryMetadata (metadata , docsOutDir , basePackageTreeJSONOutDir )
220+ err = genResourceDocsForPackageFromRegistryMetadata (* metadata , docsOutDir , basePackageTreeJSONOutDir )
166221 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 )
168223 }
169224
170- glog .Infof ("=== completed %s ===" , f .Name () )
225+ glog .Infof ("=== completed %s ===" , metadata .Name )
171226 return nil
172227 })
173228 }
174-
175229 return pool .Wait ()
176230}
177231
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+
178251func resourceDocsFromRegistryCmd () * cobra.Command {
179252 var baseDocsOutDir string
180253 var basePackageTreeJSONOutDir string
181254 var registryDir string
255+ var useAPI bool
256+ var apiURL string
182257
183258 cmd := & cobra.Command {
184259 Use : "registry [pkgName]" ,
185260 Short : "Generate resource docs for a package from the registry" ,
186261 Long : "Generate resource docs for all packages in the registry or specific packages. " +
187262 "Pass a package name in the registry as an optional arg to generate docs only for that package." ,
188263 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 )
192269 }
193270
194271 if len (args ) > 0 {
195272 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 ])
200274 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 ])
207276 }
208277
209278 docsOutDir := filepath .Join (baseDocsOutDir , metadata .Name , "api-docs" )
210-
211- err = genResourceDocsForPackageFromRegistryMetadata (metadata , docsOutDir , basePackageTreeJSONOutDir )
279+ err = genResourceDocsForPackageFromRegistryMetadata (* metadata , docsOutDir , basePackageTreeJSONOutDir )
212280 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 ] )
214282 }
215283 } else {
216284 glog .Infoln ("Generating docs for all packages in the registry..." )
217- err := genResourceDocsForAllRegistryPackages (registryDir , baseDocsOutDir , basePackageTreeJSONOutDir )
285+ err := genResourceDocsForAllRegistryPackages (provider , baseDocsOutDir , basePackageTreeJSONOutDir )
218286 if err != nil {
219287 return errors .Wrap (err , "generating docs for all packages from registry metadata" )
220288 }
@@ -234,6 +302,109 @@ func resourceDocsFromRegistryCmd() *cobra.Command {
234302 cmd .Flags ().StringVar (& registryDir , "registryDir" ,
235303 "." ,
236304 "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" )
237309
238310 return cmd
239311}
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