Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download upstream docs for dynamically bridged provider #2664

Merged
merged 16 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export PULUMI_DISABLE_AUTOMATIC_PLUGIN_ACQUISITION := true
PROJECT_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))

install_plugins::
pulumi plugin install converter terraform 1.0.19
pulumi plugin install converter terraform 1.0.20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this bump, or is it incidental?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this bump, otherwise the test won't pass.

pulumi plugin install resource random 4.16.3
pulumi plugin install resource aws 6.22.2
pulumi plugin install resource archive 0.0.4
Expand Down
4 changes: 2 additions & 2 deletions dynamic/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ require (
github.com/pgavlin/fx v0.1.6 // indirect
github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/pkg/term v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
Expand All @@ -212,7 +212,7 @@ require (
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/afero v1.9.5
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
Expand Down
33 changes: 24 additions & 9 deletions dynamic/info.go
guineveresaenger marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,15 @@ import (

func providerInfo(ctx context.Context, p run.Provider, value parameterize.Value) (tfbridge.ProviderInfo, error) {
provider := proto.New(ctx, p)
prov := tfbridge.ProviderInfo{
P: provider,
Name: p.Name(),
Version: p.Version(),
Description: "A Pulumi provider dynamically bridged from " + p.Name() + ".",
Publisher: "Pulumi",

prov := tfbridge.ProviderInfo{
P: provider,
Name: p.Name(),
Version: p.Version(),
Description: "A Pulumi provider dynamically bridged from " + p.Name() + ".",
Publisher: "Pulumi",
ResourcePrefix: inferResourcePrefix(provider),

// To avoid bogging down schema generation speed, we skip all examples.
SkipExamples: func(tfbridge.SkipExamplesArgs) bool { return true },

MetadataInfo: &tfbridge.MetadataInfo{
Path: "", Data: tfbridge.ProviderMetadata(nil),
},
Expand Down Expand Up @@ -84,6 +81,24 @@ func providerInfo(ctx context.Context, p run.Provider, value parameterize.Value)
}
},
}
// Add presumed best-effort GitHub org to the provider info.
// We do not set the GitHubOrg field for a local dynamic provider.
if value.Remote != nil {
// https://github.com/opentofu/registry/issues/1337:
// Due to discrepancies in the registry protocol/implementation,
// we infer the Terraform provider's source code repository via the following assumptions:
// - The provider's source code is hosted at github.com
// - The provider's github org, for providers, is the namespace field of the registry name
// Example:
//
// opentofu.org/provider/hashicorp/random -> "hashicorp" is deduced to be the github org.
// Note that this will only work for the provider (not the module) protocol.
urlFields := strings.Split(value.Remote.URL, "/")
ghOrg := urlFields[len(urlFields)-2]
name := urlFields[len(urlFields)-1]
prov.GitHubOrg = ghOrg
prov.Repository = "https://github.com/" + ghOrg + "/terraform-provider-" + name
}

if err := fixup.Default(&prov); err != nil {
return prov, err
Expand Down
47 changes: 44 additions & 3 deletions dynamic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"

"github.com/blang/semver"
"github.com/opentofu/opentofu/shim/run"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/spf13/afero"

"github.com/pulumi/pulumi-terraform-bridge/dynamic/parameterize"
"github.com/pulumi/pulumi-terraform-bridge/dynamic/version"
Expand Down Expand Up @@ -63,15 +66,26 @@ func initialSetup() (info.Provider, pfbridge.ProviderMetadata, func() error) {
}

var metadata pfbridge.ProviderMetadata
var fullDocs bool
metadata = pfbridge.ProviderMetadata{
XGetSchema: func(ctx context.Context, req plugin.GetSchemaRequest) ([]byte, error) {
packageSchema, err := tfgen.GenerateSchemaWithOptions(tfgen.GenerateSchemaOptions{
// Create a custom generator for schema. Examples will only be generated if `fullDocs` is set.
g, err := tfgen.NewGenerator(tfgen.GeneratorOptions{
Package: info.Name,
Version: info.Version,
Language: tfgen.Schema,
ProviderInfo: info,
DiagnosticsSink: diag.DefaultSink(os.Stdout, os.Stderr, diag.FormatOptions{
Root: afero.NewMemMapFs(),
Sink: diag.DefaultSink(os.Stdout, os.Stderr, diag.FormatOptions{
Color: colors.Always,
}),
XInMemoryDocs: true,
XInMemoryDocs: !fullDocs,
SkipExamples: !fullDocs,
})
if err != nil {
return nil, errors.Wrapf(err, "failed to create generator")
}
packageSchema, err := g.Generate()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -148,6 +162,33 @@ func initialSetup() (info.Provider, pfbridge.ProviderMetadata, func() error) {
return plugin.ParameterizeResponse{}, err
}

switch args.Remote {
case nil:
// We're using local args.
if args.Local.UpstreamRepoPath != "" {
info.UpstreamRepoPath = args.Local.UpstreamRepoPath
fullDocs = true
}
default:
fullDocs = args.Remote.Docs
if fullDocs {
// Write the upstream files at this version to a temporary directory
tmpDir, err := os.MkdirTemp("", "upstreamRepoDir")
if err != nil {
return plugin.ParameterizeResponse{}, err
}
versionTag := "v" + info.Version
cmd := exec.Command(
"git", "clone", "--depth", "1", "-b", versionTag, info.Repository, tmpDir,
)
err = cmd.Run()
if err != nil {
return plugin.ParameterizeResponse{}, err
}
info.UpstreamRepoPath = tmpDir
}
}

return plugin.ParameterizeResponse{
Name: p.Name(),
Version: v,
Expand Down
45 changes: 43 additions & 2 deletions dynamic/parameterize/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,68 @@ type RemoteArgs struct {
Name string
// Version is the (possibly empty) version constraint on the provider.
Version string
// Docs indicates if full schema documentation should be generated.
Docs bool
}

// LocalArgs represents a local TF provider referenced by path.
type LocalArgs struct {
// Path is the path to the provider binary. It can be relative or absolute.
Path string
// UpstreamRepoPath (if provided) is the local path to the dynamically bridged Terraform provider's repo.
//
// If set, full documentation will be generated for the provider.
// If not set, only documentation from the TF provider's schema will be used.
UpstreamRepoPath string
}

func ParseArgs(args []string) (Args, error) {
// Check for a leading '.' or '/' to indicate a path
if len(args) >= 1 &&
(strings.HasPrefix(args[0], "./") || strings.HasPrefix(args[0], "/")) {
if len(args) > 1 {
return Args{}, fmt.Errorf("path based providers are only parameterized by 1 argument: <path>")
docsArg := args[1]
upstreamRepoPath, found := strings.CutPrefix(docsArg, "upstreamRepoPath=")
if !found {
return Args{}, fmt.Errorf(
"path based providers are only parameterized by 2 arguments: <path> " +
"[upstreamRepoPath=<path/to/files>]",
)
}
if upstreamRepoPath == "" {
return Args{}, fmt.Errorf(
"upstreamRepoPath must be set to a non-empty value: " +
"upstreamRepoPath=path/to/files",
)
}
return Args{Local: &LocalArgs{Path: args[0], UpstreamRepoPath: upstreamRepoPath}}, nil
}
return Args{Local: &LocalArgs{Path: args[0]}}, nil
}

// This is a registry based provider
var remote RemoteArgs
switch len(args) {
// The third argument, if any, is the full docs option for when we need to generate docs
case 3:
docsArg := args[2]
errMsg := "expected third parameterized argument to be 'fullDocs=<true|false>' or be empty"

fullDocs, found := strings.CutPrefix(docsArg, "fullDocs=")
if !found {
return Args{}, fmt.Errorf("%s", errMsg)
}

switch fullDocs {
case "true":
remote.Docs = true
case "false":
// Do nothing
default:
return Args{}, fmt.Errorf("%s", errMsg)
}

fallthrough
// The second argument, if any is the version
case 2:
remote.Version = args[1]
Expand All @@ -61,6 +102,6 @@ func ParseArgs(args []string) (Args, error) {
remote.Name = args[0]
return Args{Remote: &remote}, nil
default:
return Args{}, fmt.Errorf("expected to be parameterized by 1-2 arguments: <name> [version]")
return Args{}, fmt.Errorf("expected to be parameterized by 1-3 arguments: <name> [version] [fullDocs=<true|false>]")
}
}
71 changes: 68 additions & 3 deletions dynamic/parameterize/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ func TestParseArgs(t *testing.T) {
args: []string{"./my-provider"},
expect: Args{Local: &LocalArgs{Path: "./my-provider"}},
},
{
name: "local too many args",
args: []string{"./my-provider", "nonsense"},
errMsg: autogold.Expect(
"path based providers are only parameterized by 2 arguments: <path> [upstreamRepoPath=<path/to/files>]",
),
},
{
name: "local with docs location",
args: []string{"./my-provider", "upstreamRepoPath=./my-provider"},
expect: Args{
Local: &LocalArgs{
Path: "./my-provider",
UpstreamRepoPath: "./my-provider",
},
},
},
{
name: "local empty upstreamRepoPath",
args: []string{"./my-provider", "upstreamRepoPath="},
errMsg: autogold.Expect(
"upstreamRepoPath must be set to a non-empty value: upstreamRepoPath=path/to/files",
),
},
{
name: "remote",
args: []string{"my-registry.io/typ"},
Expand All @@ -51,12 +75,53 @@ func TestParseArgs(t *testing.T) {
{
name: "no args",
args: []string{},
errMsg: autogold.Expect("expected to be parameterized by 1-2 arguments: <name> [version]"),
errMsg: autogold.Expect("expected to be parameterized by 1-3 arguments: <name> [version] [fullDocs=<true|false>]"),
},
{
name: "too many args",
args: []string{"arg1", "arg2", "arg3"},
errMsg: autogold.Expect("expected to be parameterized by 1-2 arguments: <name> [version]"),
args: []string{"arg1", "arg2", "arg3", "arg4"},
errMsg: autogold.Expect("expected to be parameterized by 1-3 arguments: <name> [version] [fullDocs=<true|false>]"),
},
{
name: "invalid third arg",
args: []string{"arg1", "arg2", "arg3"},
errMsg: autogold.Expect(
"expected third parameterized argument to be 'fullDocs=<true|false>' or be empty",
),
},
{
name: "empty third arg",
args: []string{"arg1", "arg2"},
expect: Args{Remote: &RemoteArgs{
Name: "arg1",
Version: "arg2",
Docs: false,
}},
},
{
name: "valid third arg true",
args: []string{"my-registry.io/typ", "1.2.3", "fullDocs=true"},
expect: Args{Remote: &RemoteArgs{
Name: "my-registry.io/typ",
Version: "1.2.3",
Docs: true,
}},
},
{
name: "valid third arg false",
args: []string{"my-registry.io/typ", "1.2.3", "fullDocs=false"},
expect: Args{Remote: &RemoteArgs{
Name: "my-registry.io/typ",
Version: "1.2.3",
Docs: false,
}},
},
{
name: "third arg invalid input",
args: []string{"my-registry.io/typ", "1.2.3", "fullDocs=invalid-input"},
errMsg: autogold.Expect(
"expected third parameterized argument to be 'fullDocs=<true|false>' or be empty",
),
},
}

Expand Down
43 changes: 43 additions & 0 deletions dynamic/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,49 @@ func TestSchemaGeneration(t *testing.T) { //nolint:paralleltest
testSchema("databricks/databricks", "1.50.0")
}

func TestSchemaGenerationFullDocs(t *testing.T) { //nolint:paralleltest
skipWindows(t)
type testCase struct {
name string
version string
fullDocs string
}

tc := testCase{
name: "hashicorp/random",
version: "3.6.3",
fullDocs: "fullDocs=true",
}

t.Run(strings.Join([]string{tc.name, tc.version}, "-"), func(t *testing.T) {
helper.Integration(t)
ctx := context.Background()

server := grpcTestServer(ctx, t)

result, err := server.Parameterize(ctx, &pulumirpc.ParameterizeRequest{
Parameters: &pulumirpc.ParameterizeRequest_Args{
Args: &pulumirpc.ParameterizeRequest_ParametersArgs{
Args: []string{tc.name, tc.version, tc.fullDocs},
},
},
})
require.NoError(t, err)

assert.Equal(t, tc.version, result.Version)

schema, err := server.GetSchema(ctx, &pulumirpc.GetSchemaRequest{
SubpackageName: result.Name,
SubpackageVersion: result.Version,
})

require.NoError(t, err)
var fmtSchema bytes.Buffer
require.NoError(t, json.Indent(&fmtSchema, []byte(schema.Schema), "", " "))
autogold.ExpectFile(t, autogold.Raw(fmtSchema.String()))
})
}

func TestRandomCreate(t *testing.T) {
t.Parallel()
ctx := context.Background()
Expand Down
Loading
Loading