Skip to content

Commit ffecacb

Browse files
committed
Enable fetching package metadata from pulumi registry api
address lint errors fix lint issues
1 parent f7756ae commit ffecacb

File tree

1 file changed

+212
-41
lines changed

1 file changed

+212
-41
lines changed

tools/resourcedocsgen/cmd/docs/registry.go

Lines changed: 212 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
42108
func 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+
178251
func 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

Comments
 (0)