Skip to content

Commit 8f3c1b9

Browse files
committed
Enable fetching package metadata from pulumi registry api
1 parent f7756ae commit 8f3c1b9

File tree

1 file changed

+223
-41
lines changed

1 file changed

+223
-41
lines changed

tools/resourcedocsgen/cmd/docs/registry.go

Lines changed: 223 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,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+
42113
func 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+
178256
func 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

Comments
 (0)