@@ -24,6 +24,7 @@ import (
2424 "path/filepath"
2525 "runtime"
2626 "strings"
27+ "time"
2728
2829 "github.com/spf13/cobra"
2930
@@ -39,6 +40,76 @@ 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 this from the 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+ // PackageListResponse represents the API response structure for package lists
85+ type PackageListResponse struct {
86+ Packages []PackageMetadata `json:"packages"`
87+ }
88+
89+ // NewFileSystemProvider creates a new FileSystemProvider
90+ func NewFileSystemProvider (registryDir string ) * FileSystemProvider {
91+ return & FileSystemProvider {
92+ registryDir : registryDir ,
93+ }
94+ }
95+
96+ // NewAPIProvider creates a new RegistryAPIProvider
97+ func NewAPIProvider (apiURL string ) * RegistryAPIProvider {
98+ return & RegistryAPIProvider {
99+ apiURL : apiURL ,
100+ }
101+ }
102+
103+ // contains checks if a string is in a slice
104+ func contains (slice []string , item string ) bool {
105+ for _ , s := range slice {
106+ if s == item {
107+ return true
108+ }
109+ }
110+ return false
111+ }
112+
42113func getRepoSlug (repoURL string ) (string , error ) {
43114 u , err := url .Parse (repoURL )
44115 if err != nil {
@@ -136,85 +207,87 @@ func getRegistryPackagesPath(repoPath string) string {
136207 return filepath .Join (repoPath , "themes" , "default" , "data" , "registry" , "packages" )
137208}
138209
139- func genResourceDocsForAllRegistryPackages (registryRepoPath , baseDocsOutDir , basePackageTreeJSONOutDir string ) error {
140- registryPackagesPath := getRegistryPackagesPath (registryRepoPath )
141- metadataFiles , err := os .ReadDir (registryPackagesPath )
210+ func genResourceDocsForAllRegistryPackages (
211+ provider PackageMetadataProvider ,
212+ baseDocsOutDir , basePackageTreeJSONOutDir string ,
213+ ) error {
214+ metadataList , err := provider .ListPackageMetadata ()
142215 if err != nil {
143- return errors .Wrap (err , "reading the registry packages dir " )
216+ return errors .Wrap (err , "listing package metadata " )
144217 }
145218
146219 pool := concpool .New ().WithErrors ().WithMaxGoroutines (runtime .NumCPU ())
147- for _ , f := range metadataFiles {
148- f := f
220+ for _ , metadata := range metadataList {
221+ metadata := metadata
149222 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-
223+ glog .Infof ("=== starting %s ===\n " , metadata .Name )
164224 docsOutDir := filepath .Join (baseDocsOutDir , metadata .Name , "api-docs" )
165- err = genResourceDocsForPackageFromRegistryMetadata (metadata , docsOutDir , basePackageTreeJSONOutDir )
225+ err = genResourceDocsForPackageFromRegistryMetadata (* metadata , docsOutDir , basePackageTreeJSONOutDir )
166226 if err != nil {
167- return errors .Wrapf (err , "generating resource docs using metadata file info %s" , f .Name () )
227+ return errors .Wrapf (err , "generating resource docs using metadata file info %s" , metadata .Name )
168228 }
169229
170- glog .Infof ("=== completed %s ===" , f .Name () )
230+ glog .Infof ("=== completed %s ===" , metadata .Name )
171231 return nil
172232 })
173233 }
174-
175234 return pool .Wait ()
176235}
177236
237+ func convertAPIPackageToPackageMeta (apiPkg PackageMetadata ) (* pkg.PackageMeta , error ) {
238+ return & pkg.PackageMeta {
239+ Name : apiPkg .Name ,
240+ Publisher : apiPkg .Publisher ,
241+ Description : apiPkg .Description ,
242+ LogoURL : apiPkg .LogoURL ,
243+ RepoURL : apiPkg .RepoURL ,
244+ Category : pkg .PackageCategory (apiPkg .Category ),
245+ Featured : apiPkg .IsFeatured ,
246+ Native : contains (apiPkg .PackageTypes , "native" ),
247+ Component : contains (apiPkg .PackageTypes , "component" ),
248+ PackageStatus : pkg .PackageStatus (apiPkg .PackageStatus ),
249+ SchemaFileURL : apiPkg .SchemaURL ,
250+ Version : apiPkg .Version ,
251+ Title : apiPkg .Title ,
252+ UpdatedOn : apiPkg .CreatedAt .Unix (),
253+ }, nil
254+ }
255+
178256func resourceDocsFromRegistryCmd () * cobra.Command {
179257 var baseDocsOutDir string
180258 var basePackageTreeJSONOutDir string
181259 var registryDir string
260+ var useAPI bool
261+ var apiURL string
182262
183263 cmd := & cobra.Command {
184264 Use : "registry [pkgName]" ,
185265 Short : "Generate resource docs for a package from the registry" ,
186266 Long : "Generate resource docs for all packages in the registry or specific packages. " +
187267 "Pass a package name in the registry as an optional arg to generate docs only for that package." ,
188268 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" )
269+ var provider PackageMetadataProvider
270+ if useAPI {
271+ provider = NewAPIProvider (apiURL )
272+ } else {
273+ provider = NewFileSystemProvider (registryDir )
192274 }
193275
194276 if len (args ) > 0 {
195277 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 )
278+ metadata , err := provider .GetPackageMetadata (args [0 ])
200279 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 )
280+ return errors .Wrapf (err , "getting metadata for package %q" , args [0 ])
207281 }
208282
209283 docsOutDir := filepath .Join (baseDocsOutDir , metadata .Name , "api-docs" )
210-
211- err = genResourceDocsForPackageFromRegistryMetadata (metadata , docsOutDir , basePackageTreeJSONOutDir )
284+ err = genResourceDocsForPackageFromRegistryMetadata (* metadata , docsOutDir , basePackageTreeJSONOutDir )
212285 if err != nil {
213- return errors .Wrapf (err , "generating docs for package %q from registry metadata" , pkgName )
286+ return errors .Wrapf (err , "generating docs for package %q from registry metadata" , args [ 0 ] )
214287 }
215288 } else {
216289 glog .Infoln ("Generating docs for all packages in the registry..." )
217- err := genResourceDocsForAllRegistryPackages (registryDir , baseDocsOutDir , basePackageTreeJSONOutDir )
290+ err := genResourceDocsForAllRegistryPackages (provider , baseDocsOutDir , basePackageTreeJSONOutDir )
218291 if err != nil {
219292 return errors .Wrap (err , "generating docs for all packages from registry metadata" )
220293 }
@@ -234,6 +307,115 @@ func resourceDocsFromRegistryCmd() *cobra.Command {
234307 cmd .Flags ().StringVar (& registryDir , "registryDir" ,
235308 "." ,
236309 "The root of the pulumi/registry directory" )
310+ cmd .Flags ().BoolVar (& useAPI , "use-api" , false , "Use the Pulumi Registry API instead of local files" )
311+ cmd .Flags ().StringVar (& apiURL , "api-url" ,
312+ "https://api.pulumi.com/api/preview/registry" ,
313+ "URL of the Pulumi Registry API" )
237314
238315 return cmd
239316}
317+
318+ // GetPackageMetadata implements PackageMetadataProvider for FileSystemProvider
319+ func (p * FileSystemProvider ) GetPackageMetadata (pkgName string ) (* pkg.PackageMeta , error ) {
320+ metadataFilePath := filepath .Join (getRegistryPackagesPath (p .registryDir ), pkgName + ".yaml" )
321+ b , err := os .ReadFile (metadataFilePath )
322+ if err != nil {
323+ return nil , errors .Wrapf (err , "reading the metadata file %s" , metadataFilePath )
324+ }
325+
326+ var metadata pkg.PackageMeta
327+ if err := yaml .Unmarshal (b , & metadata ); err != nil {
328+ return nil , errors .Wrapf (err , "unmarshalling the metadata file %s" , metadataFilePath )
329+ }
330+
331+ return & metadata , nil
332+ }
333+
334+ // ListPackageMetadata implements PackageMetadataProvider for FileSystemProvider
335+ func (p * FileSystemProvider ) ListPackageMetadata () ([]* pkg.PackageMeta , error ) {
336+ registryPackagesPath := getRegistryPackagesPath (p .registryDir )
337+ files , err := os .ReadDir (registryPackagesPath )
338+ if err != nil {
339+ return nil , errors .Wrapf (err , "reading directory %s" , registryPackagesPath )
340+ }
341+
342+ // Count YAML files to pre-allocate the slice mostly to appease the linter.
343+ var metadataCount int
344+ for _ , file := range files {
345+ if strings .HasSuffix (file .Name (), ".yaml" ) {
346+ metadataCount ++
347+ }
348+ }
349+
350+ metadataList := make ([]* pkg.PackageMeta , 0 , metadataCount )
351+ for _ , file := range files {
352+ if ! strings .HasSuffix (file .Name (), ".yaml" ) {
353+ continue
354+ }
355+
356+ metadata , err := p .GetPackageMetadata (strings .TrimSuffix (file .Name (), ".yaml" ))
357+ if err != nil {
358+ return nil , err
359+ }
360+ metadataList = append (metadataList , metadata )
361+ }
362+
363+ return metadataList , nil
364+ }
365+
366+ // GetPackageMetadata implements PackageMetadataProvider for RegistryAPIProvider
367+ func (p * RegistryAPIProvider ) GetPackageMetadata (pkgName string ) (* pkg.PackageMeta , error ) {
368+ resp , err := http .Get (fmt .Sprintf ("%s/packages?name=%s" , p .apiURL , pkgName ))
369+ if err != nil {
370+ return nil , errors .Wrapf (err , "fetching package metadata from API for %s" , pkgName )
371+ }
372+ defer resp .Body .Close ()
373+
374+ if resp .StatusCode != http .StatusOK {
375+ return nil , errors .Errorf ("unexpected status code %d when fetching package metadata" , resp .StatusCode )
376+ }
377+
378+ var response PackageListResponse
379+ if err := json .NewDecoder (resp .Body ).Decode (& response ); err != nil {
380+ return nil , errors .Wrap (err , "decoding API response" )
381+ }
382+
383+ if len (response .Packages ) == 0 {
384+ return nil , errors .Errorf ("no package found with name %s" , pkgName )
385+ }
386+
387+ if len (response .Packages ) > 1 {
388+ return nil , errors .Errorf ("multiple packages found with name %s" , pkgName )
389+ }
390+
391+ return convertAPIPackageToPackageMeta (response .Packages [0 ])
392+ }
393+
394+ // ListPackageMetadata implements PackageMetadataProvider for RegistryAPIProvider
395+ func (p * RegistryAPIProvider ) ListPackageMetadata () ([]* pkg.PackageMeta , error ) {
396+ resp , err := http .Get (p .apiURL + "/packages" )
397+ if err != nil {
398+ return nil , errors .Wrap (err , "fetching package list from API" )
399+ }
400+ defer resp .Body .Close ()
401+
402+ if resp .StatusCode != http .StatusOK {
403+ return nil , errors .Errorf ("unexpected status code %d when fetching package list" , resp .StatusCode )
404+ }
405+
406+ var response PackageListResponse
407+ if err := json .NewDecoder (resp .Body ).Decode (& response ); err != nil {
408+ return nil , errors .Wrap (err , "decoding API response" )
409+ }
410+
411+ metadataList := make ([]* pkg.PackageMeta , 0 , len (response .Packages ))
412+ for _ , apiPkg := range response .Packages {
413+ metadata , err := convertAPIPackageToPackageMeta (apiPkg )
414+ if err != nil {
415+ return nil , err
416+ }
417+ metadataList = append (metadataList , metadata )
418+ }
419+
420+ return metadataList , nil
421+ }
0 commit comments