diff --git a/upgrade/http.go b/upgrade/http.go new file mode 100644 index 0000000..44017b8 --- /dev/null +++ b/upgrade/http.go @@ -0,0 +1,73 @@ +// HTTP-related effects. +package upgrade + +import ( + "context" + "fmt" + "io" + "net/http" + "time" +) + +// Use this function in steps to perform HTTP GET. +func getHTTP(ctx context.Context, url string) ([]byte, error) { + h, ok := ctx.Value(httpHandlerKey).(httpHandler) + if !ok { + h = &defaultHttpHandler{ + retryAttempts: 5, + delay: func(attempt int) time.Duration { + n := (attempt + 1) * (attempt + 1) + return time.Duration(n) * time.Second + }, + } + } + return h.getHTTP(url) +} + +// Set this to a mock in tests to avoid hitting actual HTTP. +type httpHandler interface { + getHTTP(url string) ([]byte, error) +} + +var httpHandlerKey = struct{}{} + +type defaultHttpHandler struct { + retryAttempts int + delay func(attempt int) time.Duration +} + +func (*defaultHttpHandler) tryGetHTTP(url string) (content []byte, finalError error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil && finalError == nil { + finalError = err + } + }() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Non-200 code for GET %v: %v", url, resp.StatusCode) + } + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if err := resp.Body.Close(); err != nil { + return nil, err + } + return respBytes, nil +} + +func (h *defaultHttpHandler) getHTTP(url string) ([]byte, error) { + var c []byte + var err error + for attempt := 0; attempt < h.retryAttempts; attempt++ { + c, err = h.tryGetHTTP(url) + if err == nil { + return c, nil + } + time.Sleep(h.delay(attempt)) + } + return nil, err +} diff --git a/upgrade/steps.go b/upgrade/steps.go index 041f12e..96cd2e1 100644 --- a/upgrade/steps.go +++ b/upgrade/steps.go @@ -1002,51 +1002,53 @@ var planBridgeUpgrade = stepv2.Func11E("Planning Bridge Upgrade", func( }) var planPluginSDKUpgrade = stepv2.Func12E("Planning Plugin SDK Upgrade", func( - ctx context.Context, repo ProviderRepo, + ctx context.Context, bridgeRef string, ) (_, display string, _ error) { defer func() { stepv2.SetLabel(ctx, display) }() - current, ok := originalGoVersionOfV2(ctx, repo, "provider/go.mod", - "github.com/pulumi/terraform-plugin-sdk/v2") - if !ok { - return "", "not found", nil + + sdkv2 := "github.com/hashicorp/terraform-plugin-sdk/v2" + + br, err := ParseRef(bridgeRef) + if err != nil { + return "", "", fmt.Errorf("cannot parse a Git bridge ref: %v", bridgeRef) + } + + var r string + switch br := br.(type) { + case *Version: + r = "v" + br.SemVer.String() + case *HashReference: + r = br.GitHash + case *Latest: + contract.Failf("Unsupported `latest` Ref") + default: + contract.Failf("Unsupported type of Ref: incomplete case match") } - refs := gitRefsOfV2(ctx, - "https://github.com/pulumi/terraform-plugin-sdk.git", "heads") - currentRef, err := module.PseudoVersionRev(current.Version) + + url := fmt.Sprintf("https://raw.githubusercontent.com/pulumi/pulumi-terraform-bridge/%s/go.mod", r) + + gomodBytes, err := getHTTP(ctx, url) if err != nil { - return "", "", fmt.Errorf("unable to parse PseudoVersionRef %q: %w", - current.Version, err) + return "", "", fmt.Errorf("Failed to get %v: %w", url, err) } - latest := latestSemverTag("upstream-", refs) - currentBranch, ok := refs.labelOf(currentRef) - if !ok { - // use latest versioned branch - // This is not quite correct, since it could be newer than the - // version used in the bridge. - // TODO: https://github.com/pulumi/upgrade-provider/issues/245 - latestSha, ok := refs.shaOf(fmt.Sprintf("refs/heads/upstream-%s", latest.Original())) - contract.Assertf(ok, "Failed to lookup sha of known tag: %q not in %#v", latest.Original(), refs.labelToRef) - return latestSha, fmt.Sprintf("Could not find head branch at ref %s. Upgrading to "+ - "latest branch at %s instead.", currentRef, latest), nil + + goMod, err := modfile.Parse("go.mod", gomodBytes, nil) + if err != nil { + return "", "", fmt.Errorf("Failed parse go.mod: %w", err) } - trim := func(branch string) string { - const p = "refs/heads/upstream-" - return strings.TrimPrefix(branch, p) + version := "" + for _, re := range goMod.Replace { + if re.Old.Path == sdkv2 { + version = re.New.Version + } } - currentBranch = trim(currentBranch) - // We are guaranteed to get a non-nil result because there - // are semver tags released tags with this prefix. - if latest.Original() == currentBranch { - return "", fmt.Sprintf("Up to date at %s", latest), nil + if version == "" { + return "", "", fmt.Errorf("Failed to find %v replace in bridge go.mod", sdkv2) } - latestTag := fmt.Sprintf("refs/heads/upstream-%s", latest.Original()) - latestSha, ok := refs.shaOf(latestTag) - contract.Assertf(ok, "Failed to lookup sha of known tag: %q not in %#v", - latestTag, refs.labelToRef) - return latestSha, fmt.Sprintf("%s -> %s", currentBranch, latest), nil + return version, fmt.Sprintf("bridge %s needs terraform-plugin-sdk %s", bridgeRef, version), nil }) var plantPfUpgrade = stepv2.Func11E("Planning Plugin Framework Upgrade", func( diff --git a/upgrade/steps_helpers.go b/upgrade/steps_helpers.go index ee5ca7b..0cc783b 100644 --- a/upgrade/steps_helpers.go +++ b/upgrade/steps_helpers.go @@ -383,15 +383,6 @@ func (g gitRepoRefs) shaOf(label string) (string, bool) { return "", false } -func (g gitRepoRefs) labelOf(sha string) (string, bool) { - for label, ref := range g.labelToRef { - if strings.HasPrefix(ref, sha) { - return label, true - } - } - return "", false -} - func (g gitRepoRefs) sortedLabels(less func(string, string) bool) []string { labels := make([]string, 0, len(g.labelToRef)) for label := range g.labelToRef { diff --git a/upgrade/steps_test.go b/upgrade/steps_test.go index cbab0d6..dfd9009 100644 --- a/upgrade/steps_test.go +++ b/upgrade/steps_test.go @@ -359,85 +359,41 @@ func TestCheckMaintenancePatchExpiredCadence(t *testing.T) { ]`), "Check if we should release a maintenance patch", maintenanceRelease) } -func TestPluginSDKUpgradeLatest(t *testing.T) { - testReplay((&Context{ - GoPath: "/Users/myuser/go", - }).Wrap(context.Background()), - t, jsonMarshal[[]*step.Step](t, `[ - { - "name": "Planning Plugin SDK Upgrade", - "inputs": [ - { - "Name": "pulumi-keycloak", - "Org": "pulumi" - } - ], - "outputs": [ - "74776a5cd5f9a1330c34124588e0ad800d26724d", - "Could not find head branch at ref e6d96b3b8f7e. Upgrading to latest branch at 2.29.0 instead.", - null - ] - }, - { - "name": "Original Go Version of", - "inputs": [ - { - "Name": "pulumi-keycloak", - "Org": "pulumi" - }, - "provider/go.mod", - "github.com/pulumi/terraform-plugin-sdk/v2" - ], - "outputs": [ - { - "Path": "github.com/pulumi/terraform-plugin-sdk/v2", - "Version": "v2.0.0-20230912190043-e6d96b3b8f7e" - }, - true, - null - ] - }, - { - "name": "git", - "inputs": [ - "git", - [ - "show", - ":provider/go.mod" - ] - ], - "outputs": [ - "module github.com/pulumi/pulumi-keycloak/provider/v5\n\ngo 1.21\n\nreplace (\n\tgithub.com/hashicorp/terraform-plugin-sdk/v2 =\u003e github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20230912190043-e6d96b3b8f7e\n\tgithub.com/hashicorp/vault =\u003e github.com/hashicorp/vault v1.2.0\n\tgithub.com/mrparkers/terraform-provider-keycloak =\u003e ../upstream\n)\n\nrequire (\n\tgithub.com/mrparkers/terraform-provider-keycloak v0.0.0-00010101000000-000000000000\n\tgithub.com/pulumi/pulumi-terraform-bridge/v3 v3.72.0\n\tgithub.com/pulumi/pulumi/sdk/v3 v3.103.1\n)\n\nrequire (\n\tcloud.google.com/go v0.110.8 // indirect\n\tcloud.google.com/go/compute v1.23.0 // indirect\n)\n", - null - ], - "impure": true - }, - { - "name": "git refs of", - "inputs": [ - "https://github.com/pulumi/terraform-plugin-sdk.git", - "heads" - ], - "outputs": [ - {}, - null - ] - }, +func TestPluginSDKUpgrade(t *testing.T) { + ctx := context.WithValue(context.Background(), httpHandlerKey, simpleHttpHandler(func(url string) ([]byte, error) { + if url == "https://raw.githubusercontent.com/pulumi/pulumi-terraform-bridge/v3.73.0/go.mod" { + return []byte(` +module github.com/pulumi/pulumi-terraform-bridge/v3 + +go 1.20 + +replace github.com/pulumi/pulumi-terraform-bridge/x/muxer => ./x/muxer + +replace github.com/hashicorp/terraform-plugin-sdk/v2 => github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20240129205329-74776a5cd5f9 +`), nil + } + return nil, fmt.Errorf("not found") + })) + testReplay((&Context{GoPath: "/Users/myuser/go"}).Wrap(ctx), t, jsonMarshal[[]*step.Step](t, ` + [ { - "name": "git", - "inputs": [ - "git", - [ - "ls-remote", - "--heads", - "https://github.com/pulumi/terraform-plugin-sdk.git" - ] - ], - "outputs": [ - "efd600e5c6b7a15badde5c32d6b16312c8823409\trefs/heads/dependabot/go_modules/golang.org/x/crypto-0.17.0\n313a7c4cbcad744ab88abc26b42f8e526680ce49\trefs/heads/dependabot/go_modules/golang.org/x/net-0.17.0\ndacd5f7afbedcd7aeb5881b94573369385692784\trefs/heads/dependabot/go_modules/google.golang.org/grpc-1.56.3\n7ac578ce47fc07e0888beee6d4ab9db09369274f\trefs/heads/master\na81109190d574226079b2ceff408cc7bca82f6fe\trefs/heads/pgavlin/upstream-v2.12.0\n430f685de305a148a5a79d1c3959ecf109986675\trefs/heads/upstream-v2.24.1\n3fa930f865709d507154454c2af20e023d15b6e6\trefs/heads/upstream-v2.26.1\n03a71d0fca3d7d5ff24a52e334aa2e52f442fe04\trefs/heads/upstream-v2.27.0\n74776a5cd5f9a1330c34124588e0ad800d26724d\trefs/heads/upstream-v2.29.0\n", - null - ], - "impure": true + "name": "Planning Plugin SDK Upgrade", + "inputs": [ + "3.73.0" + ], + "outputs": [ + "v2.0.0-20240129205329-74776a5cd5f9", + "bridge 3.73.0 needs terraform-plugin-sdk v2.0.0-20240129205329-74776a5cd5f9", + null + ] } -]`), "Planning Plugin SDK Upgrade", planPluginSDKUpgrade) + ]`), "Planning Plugin SDK Upgrade", planPluginSDKUpgrade) +} + +type simpleHttpHandler func(string) ([]byte, error) + +var _ httpHandler = (*simpleHttpHandler)(nil) + +func (sh simpleHttpHandler) getHTTP(url string) ([]byte, error) { + return sh(url) } diff --git a/upgrade/upgrade_provider.go b/upgrade/upgrade_provider.go index 8890ec2..67133cc 100644 --- a/upgrade/upgrade_provider.go +++ b/upgrade/upgrade_provider.go @@ -100,7 +100,7 @@ func UpgradeProvider(ctx context.Context, repoOrg, repoName string) (err error) err = stepv2.PipelineCtx(ctx, "Plan Upgrade", func(ctx context.Context) { if GetContext(ctx).UpgradeBridgeVersion { targetBridgeVersion = planBridgeUpgrade(ctx, goMod) - tfSDKTargetSHA, tfSDKUpgrade = planPluginSDKUpgrade(ctx, repo) + tfSDKTargetSHA, tfSDKUpgrade = planPluginSDKUpgrade(ctx, targetBridgeVersion.String()) // Check if we need to release a maintenance patch and set context if so GetContext(ctx).MaintenancePatch = maintenanceRelease(ctx, repo) }