Skip to content

Commit e8dac29

Browse files
sean1588iwahbe
authored andcommitted
Enable fetching package metadata from pulumi registry api
1 parent ef104a5 commit e8dac29

File tree

4 files changed

+685
-44
lines changed

4 files changed

+685
-44
lines changed

tools/resourcedocsgen/cmd/docs/registry.go

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
package docs
1616

1717
import (
18+
"context"
1819
"encoding/json"
1920
"fmt"
2021
"io"
2122
"net/http"
2223
"net/url"
23-
"os"
2424
"path/filepath"
2525
"runtime"
2626
"strings"
@@ -36,6 +36,7 @@ import (
3636
pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema"
3737
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
3838
"github.com/pulumi/registry/tools/resourcedocsgen/pkg"
39+
"github.com/pulumi/registry/tools/resourcedocsgen/pkg/registry/svc"
3940
concpool "github.com/sourcegraph/conc/pool"
4041
)
4142

@@ -132,89 +133,69 @@ func getSchemaFileURL(metadata pkg.PackageMeta) (string, error) {
132133
return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s", repoSlug, metadata.Version, schemaFilePath), nil
133134
}
134135

135-
func getRegistryPackagesPath(repoPath string) string {
136-
return filepath.Join(repoPath, "themes", "default", "data", "registry", "packages")
137-
}
138-
139-
func genResourceDocsForAllRegistryPackages(registryRepoPath, baseDocsOutDir, basePackageTreeJSONOutDir string) error {
140-
registryPackagesPath := getRegistryPackagesPath(registryRepoPath)
141-
metadataFiles, err := os.ReadDir(registryPackagesPath)
136+
func genResourceDocsForAllRegistryPackages(
137+
ctx context.Context,
138+
provider svc.PackageMetadataProvider,
139+
baseDocsOutDir, basePackageTreeJSONOutDir string,
140+
) error {
141+
metadataList, err := provider.ListPackageMetadata(ctx)
142142
if err != nil {
143-
return errors.Wrap(err, "reading the registry packages dir")
143+
return errors.Wrap(err, "listing package metadata")
144144
}
145145

146146
pool := concpool.New().WithErrors().WithMaxGoroutines(runtime.NumCPU())
147-
for _, f := range metadataFiles {
148-
f := f
147+
for _, metadata := range metadataList {
149148
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-
149+
glog.Infof("=== starting %s ===\n", metadata.Name)
164150
docsOutDir := filepath.Join(baseDocsOutDir, metadata.Name, "api-docs")
165151
err = genResourceDocsForPackageFromRegistryMetadata(metadata, docsOutDir, basePackageTreeJSONOutDir)
166152
if err != nil {
167-
return errors.Wrapf(err, "generating resource docs using metadata file info %s", f.Name())
153+
return errors.Wrapf(err, "generating resource docs using metadata file info %s", metadata.Name)
168154
}
169155

170-
glog.Infof("=== completed %s ===", f.Name())
156+
glog.Infof("=== completed %s ===", metadata.Name)
171157
return nil
172158
})
173159
}
174-
175160
return pool.Wait()
176161
}
177162

178163
func resourceDocsFromRegistryCmd() *cobra.Command {
179164
var baseDocsOutDir string
180165
var basePackageTreeJSONOutDir string
181166
var registryDir string
167+
var useAPI bool
168+
var apiURL string
182169

183170
cmd := &cobra.Command{
184171
Use: "registry [pkgName]",
185172
Short: "Generate resource docs for a package from the registry",
186173
Long: "Generate resource docs for all packages in the registry or specific packages. " +
187174
"Pass a package name in the registry as an optional arg to generate docs only for that package.",
188175
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")
176+
ctx := cmd.Context()
177+
var provider svc.PackageMetadataProvider
178+
if useAPI {
179+
provider = svc.NewAPIProvider(apiURL)
180+
} else {
181+
provider = svc.NewFileSystemProvider(registryDir)
192182
}
193183

194184
if len(args) > 0 {
195185
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)
186+
metadata, err := provider.GetPackageMetadata(ctx, args[0])
200187
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)
188+
return errors.Wrapf(err, "getting metadata for package %q", args[0])
207189
}
208190

209191
docsOutDir := filepath.Join(baseDocsOutDir, metadata.Name, "api-docs")
210-
211192
err = genResourceDocsForPackageFromRegistryMetadata(metadata, docsOutDir, basePackageTreeJSONOutDir)
212193
if err != nil {
213-
return errors.Wrapf(err, "generating docs for package %q from registry metadata", pkgName)
194+
return errors.Wrapf(err, "generating docs for package %q from registry metadata", args[0])
214195
}
215196
} else {
216197
glog.Infoln("Generating docs for all packages in the registry...")
217-
err := genResourceDocsForAllRegistryPackages(registryDir, baseDocsOutDir, basePackageTreeJSONOutDir)
198+
err := genResourceDocsForAllRegistryPackages(ctx, provider, baseDocsOutDir, basePackageTreeJSONOutDir)
218199
if err != nil {
219200
return errors.Wrap(err, "generating docs for all packages from registry metadata")
220201
}
@@ -234,6 +215,10 @@ func resourceDocsFromRegistryCmd() *cobra.Command {
234215
cmd.Flags().StringVar(&registryDir, "registryDir",
235216
".",
236217
"The root of the pulumi/registry directory")
218+
cmd.Flags().BoolVar(&useAPI, "use-api", false, "Use the Pulumi Registry API instead of local files")
219+
cmd.Flags().StringVar(&apiURL, "api-url",
220+
"https://api.pulumi.com/api/preview/registry",
221+
"URL of the Pulumi Registry API")
237222

238223
return cmd
239224
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2025, Pulumi Corporation.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package svc
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
"net/http"
22+
"slices"
23+
"time"
24+
25+
"github.com/pkg/errors"
26+
27+
"github.com/pulumi/registry/tools/resourcedocsgen/pkg"
28+
)
29+
30+
// NewAPIProvider creates a new PackageMetadataProvider that reads from the Pulumi API
31+
func NewAPIProvider(apiURL string) PackageMetadataProvider {
32+
return &registryAPIProvider{
33+
apiURL: apiURL,
34+
client: http.DefaultClient,
35+
}
36+
}
37+
38+
// registryAPIProvider implements PackageMetadataProvider using the Pulumi API
39+
// to retrieve package metadata.
40+
type registryAPIProvider struct {
41+
apiURL string
42+
client *http.Client
43+
}
44+
45+
// apiPackageMetadata represents the API response structure for package metadata
46+
// from the Pulumi Registry API.
47+
type apiPackageMetadata struct {
48+
Name string `json:"name"`
49+
Publisher string `json:"publisher"`
50+
Source string `json:"source"`
51+
Version string `json:"version"`
52+
Title string `json:"title,omitempty"`
53+
Description string `json:"description,omitempty"`
54+
LogoURL string `json:"logoUrl,omitempty"`
55+
RepoURL string `json:"repoUrl,omitempty"`
56+
Category string `json:"category,omitempty"`
57+
IsFeatured bool `json:"isFeatured"`
58+
PackageTypes []string `json:"packageTypes,omitempty"`
59+
PackageStatus string `json:"packageStatus"`
60+
SchemaURL string `json:"schemaURL"`
61+
CreatedAt time.Time `json:"createdAt"`
62+
}
63+
64+
// packageListResponse represents the API response structure for package lists
65+
type packageListResponse struct {
66+
Packages []apiPackageMetadata `json:"packages"`
67+
}
68+
69+
func convertAPIPackageToPackageMeta(apiPkg apiPackageMetadata) pkg.PackageMeta {
70+
return pkg.PackageMeta{
71+
Name: apiPkg.Name,
72+
Publisher: apiPkg.Publisher,
73+
Description: apiPkg.Description,
74+
LogoURL: apiPkg.LogoURL,
75+
RepoURL: apiPkg.RepoURL,
76+
Category: pkg.PackageCategory(apiPkg.Category),
77+
Featured: apiPkg.IsFeatured,
78+
Native: slices.Contains(apiPkg.PackageTypes, "native"),
79+
Component: slices.Contains(apiPkg.PackageTypes, "component"),
80+
PackageStatus: pkg.PackageStatus(apiPkg.PackageStatus),
81+
SchemaFileURL: apiPkg.SchemaURL,
82+
Version: apiPkg.Version,
83+
Title: apiPkg.Title,
84+
UpdatedOn: apiPkg.CreatedAt.Unix(),
85+
}
86+
}
87+
88+
// GetPackageMetadata implements PackageMetadataProvider for registryAPIProvider
89+
func (p *registryAPIProvider) GetPackageMetadata(ctx context.Context, pkgName string) (pkg.PackageMeta, error) {
90+
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
91+
fmt.Sprintf("%s/packages?name=%s", p.apiURL, pkgName), nil)
92+
if err != nil {
93+
return pkg.PackageMeta{}, errors.Wrapf(err, "creating request for package %s", pkgName)
94+
}
95+
96+
resp, err := p.client.Do(req)
97+
if err != nil {
98+
return pkg.PackageMeta{}, errors.Wrapf(err, "fetching package metadata from API for %s", pkgName)
99+
}
100+
defer resp.Body.Close()
101+
102+
if resp.StatusCode != http.StatusOK {
103+
return pkg.PackageMeta{}, errors.Errorf("unexpected status code %d when fetching package metadata", resp.StatusCode)
104+
}
105+
106+
var response packageListResponse
107+
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
108+
return pkg.PackageMeta{}, errors.Wrap(err, "decoding API response")
109+
}
110+
111+
switch len(response.Packages) {
112+
case 0:
113+
return pkg.PackageMeta{}, errors.Errorf("no package found with name %s", pkgName)
114+
case 1:
115+
metadata := convertAPIPackageToPackageMeta(response.Packages[0])
116+
return metadata, nil
117+
default:
118+
return pkg.PackageMeta{}, errors.Errorf("multiple packages found with name %s", pkgName)
119+
}
120+
}
121+
122+
// ListPackageMetadata implements PackageMetadataProvider for registryAPIProvider
123+
func (p *registryAPIProvider) ListPackageMetadata(ctx context.Context) ([]pkg.PackageMeta, error) {
124+
var allPackages []pkg.PackageMeta
125+
// Maximum allowed by the API (must be less than 500). Request up to 499 to
126+
// account for pagination with minimum number of requests.
127+
const limit = 499
128+
continuationToken := ""
129+
130+
for {
131+
url := fmt.Sprintf("%s/packages?limit=%d", p.apiURL, limit)
132+
if continuationToken != "" {
133+
url = fmt.Sprintf("%s&continuationToken=%s", url, continuationToken)
134+
}
135+
136+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
137+
if err != nil {
138+
return nil, errors.Wrap(err, "creating request for package list")
139+
}
140+
141+
resp, err := p.client.Do(req)
142+
if err != nil {
143+
return nil, errors.Wrap(err, "fetching package list from API")
144+
}
145+
defer resp.Body.Close()
146+
147+
if resp.StatusCode != http.StatusOK {
148+
return nil, errors.Errorf("unexpected status code %d when fetching package list", resp.StatusCode)
149+
}
150+
151+
var response struct {
152+
Packages []apiPackageMetadata `json:"packages"`
153+
ContinuationToken *string `json:"continuationToken"`
154+
}
155+
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
156+
return nil, errors.Wrap(err, "decoding API response")
157+
}
158+
159+
for _, apiPkg := range response.Packages {
160+
metadata := convertAPIPackageToPackageMeta(apiPkg)
161+
allPackages = append(allPackages, metadata)
162+
}
163+
164+
// If there's no continuation token, we've reached the end
165+
if response.ContinuationToken == nil {
166+
break
167+
}
168+
169+
continuationToken = *response.ContinuationToken
170+
}
171+
172+
return allPackages, nil
173+
}

0 commit comments

Comments
 (0)