Skip to content

Commit b9e1a2c

Browse files
committed
Consolidate go modules for bridged providers
1 parent 47628ee commit b9e1a2c

File tree

57 files changed

+497
-27
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+497
-27
lines changed

.github/workflows/update-workflows.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ jobs:
7676
rm pulumi-${{ inputs.provider_name }}/.github/workflows/master.yml || echo "not found"
7777
- name: Generate workflow files into pulumi-${{ inputs.provider_name }}
7878
if: inputs.bridged
79-
run: |
80-
cd ci-mgmt/provider-ci && go run ./... generate \
81-
--config ../../pulumi-${{ inputs.provider_name }}/.ci-mgmt.yaml \
82-
--out ../../pulumi-${{ inputs.provider_name }}
79+
run: go run github.com/pulumi/ci-mgmt/provider-ci@${{ github.sha }} generate
80+
shell: bash
81+
working-directory: pulumi-${{ inputs.provider_name }}
8382
- name: Copy files from ci-mgmt to pulumi-${{ inputs.provider_name }}
8483
if: ${{ inputs.bridged != true }}
8584
run: |

provider-ci/Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ACTIONLINT_VERSION := 1.6.24
77
ACTIONLINT := bin/actionlint-$(ACTIONLINT_VERSION)
88

99
.PHONY: all test gen ensure
10-
all: ensure test format lint
10+
all: ensure test format lint unit
1111
test: test-providers
1212
gen: test-providers
1313
ensure:: bin/provider-ci $(ACTIONLINT)
@@ -20,7 +20,7 @@ $(ACTIONLINT):
2020
mv bin/actionlint $(ACTIONLINT)
2121

2222
# Basic helper targets.
23-
.PHONY: clean lint format
23+
.PHONY: clean lint format unit
2424
clean:
2525
rm -rf bin
2626

@@ -30,6 +30,9 @@ lint:
3030
format:
3131
go fmt ./...
3232

33+
unit:
34+
go test -v ./...
35+
3336
# We check in a subset of provider workflows so template changes are visible in PR diffs.
3437
#
3538
# This provides an example of generated providers for PR reviewers. This target

provider-ci/go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module github.com/pulumi/ci-mgmt/provider-ci
22

3-
go 1.21
3+
go 1.23.3
44

55
require (
66
github.com/Masterminds/sprig v2.22.0+incompatible
77
github.com/spf13/cobra v1.7.0
8+
github.com/stretchr/testify v1.7.0
89
gopkg.in/yaml.v3 v3.0.1
910
)
1011

@@ -19,8 +20,8 @@ require (
1920
github.com/kr/pretty v0.1.0 // indirect
2021
github.com/mitchellh/copystructure v1.2.0 // indirect
2122
github.com/mitchellh/reflectwalk v1.0.2 // indirect
23+
github.com/pmezard/go-difflib v1.0.0 // indirect
2224
github.com/spf13/pflag v1.0.5 // indirect
23-
github.com/stretchr/testify v1.7.0 // indirect
2425
golang.org/x/crypto v0.17.0 // indirect
2526
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
2627
)
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package migrations
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
// consolidateModules moves ./provider/go.mod to the repository root (./go.mod)
13+
// and consolidates it with ./examples/go.mod and ./tests/go.mod (if they
14+
// exist). The SDK module is untouched, so SDK consumers are unaffected.
15+
//
16+
// The migration simplifies dependency management; eliminates the need for
17+
// `replace` directives (except for shims); ensures consistent package
18+
// versioning between provider logic and tests; makes it easier to share code;
19+
// yields better IDE integration; and is all-around easier to work with.
20+
//
21+
// This was initially motivated by work to shard our integration tests. Our old
22+
// module structure sometimes forced us to put integration tests alongside unit
23+
// tests under ./provider. We also had integration tests under ./examples.
24+
// Being able to shard both of those things concurrently (as part of a single
25+
// `go test` command) wasn't possible due to them existing in separate modules.
26+
//
27+
// See also: https://go.dev/wiki/Modules#should-i-have-multiple-modules-in-a-single-repository
28+
type consolidateModules struct{}
29+
30+
func (consolidateModules) Name() string {
31+
return "Consolidate Go modules"
32+
}
33+
34+
func (consolidateModules) ShouldRun(_ string) bool {
35+
_, err := os.Stat("provider/go.mod")
36+
return err == nil // Exists.
37+
}
38+
39+
func (consolidateModules) Migrate(_, outDir string) error {
40+
run := func(args ...string) ([]byte, error) {
41+
cmd := exec.Command(args[0], args[1:]...)
42+
cmd.Dir = outDir
43+
cmd.Stderr = os.Stderr
44+
return cmd.Output()
45+
}
46+
47+
// Move provider's module down.
48+
if _, err := run("git", "mv", "-f", "provider/go.mod", "go.mod"); err != nil {
49+
return fmt.Errorf("moving provider/go.mod: %w", err)
50+
}
51+
if _, err := run("git", "mv", "-f", "provider/go.sum", "go.sum"); err != nil {
52+
return fmt.Errorf("moving provider/go.sum: %w", err)
53+
}
54+
55+
// Load the module as JSON.
56+
out, err := run("go", "mod", "edit", "-json", "go.mod")
57+
if err != nil {
58+
return fmt.Errorf("exporting go.mod: %w", err)
59+
}
60+
var mod gomod
61+
err = json.Unmarshal(out, &mod)
62+
if err != nil {
63+
return fmt.Errorf("reading go.mod: %w", err)
64+
}
65+
66+
// Move relative `replace` paths up or down a directory.
67+
for idx, r := range mod.Replace {
68+
if strings.HasPrefix(r.New.Path, "../") {
69+
r.New.Path = strings.Replace(r.New.Path, "../", "./", 1)
70+
} else if strings.HasPrefix(r.New.Path, "./") {
71+
r.New.Path = strings.Replace(r.New.Path, "./", "./provider/", 1)
72+
}
73+
if r.New.Path == mod.Replace[idx].New.Path {
74+
continue // Unchanged.
75+
}
76+
77+
// Commit the changes.
78+
old := r.Old.Path
79+
if r.Old.Version != "" {
80+
old += "@" + r.Old.Version
81+
}
82+
_, err = run("go", "mod", "edit", fmt.Sprintf("-replace=%s=%s", old, r.New.Path))
83+
if err != nil {
84+
return fmt.Errorf("replacing %q: %w", old, err)
85+
}
86+
}
87+
88+
// Remove examples/tests modules. We'll recover their requirements with a
89+
// `tidy` at the end. It's OK if these don't exist.
90+
_, _ = run("git", "rm", "examples/go.mod")
91+
_, _ = run("git", "rm", "examples/go.sum")
92+
_, _ = run("git", "rm", "tests/go.mod")
93+
_, _ = run("git", "rm", "tests/go.sum")
94+
95+
// Rewrite our module path and determine our new import, if it's changed.
96+
//
97+
// The module `github.com/pulumi/pulumi-foo/provider/v6` becomes
98+
// `github.com/pulumi/pulumi-foo/v6` and existing code should be imported
99+
// as `github.com/pulumi/pulumi-foo/v6/provider`.
100+
//
101+
// For v1 modules, `github.com/pulumi/pulumi-foo/provider` becomes
102+
// `github.com/pulumi/pulumi-foo` and existing imports are unchanged.
103+
104+
oldImport := mod.Module.Path
105+
newModule := filepath.Dir(oldImport) // Strip "/vN" or "/provider".
106+
newImport := oldImport
107+
108+
// Handle major version.
109+
if base := filepath.Base(oldImport); base != "provider" {
110+
if !strings.HasPrefix(base, "v") {
111+
return fmt.Errorf("expected a major version, got %q", base)
112+
}
113+
newModule = filepath.Join(filepath.Dir(newModule), base)
114+
newImport = filepath.Join(newModule, "provider")
115+
}
116+
117+
// Update our module name.
118+
_, err = run("go", "mod", "edit", "-module="+newModule)
119+
if err != nil {
120+
return fmt.Errorf("rewriting module name: %w", err)
121+
}
122+
123+
// Re-write imports for our provider, examples, and tests modules.
124+
rewriteImport := func(oldImport, newImport string) error {
125+
if oldImport == newImport {
126+
return nil // Nothing to do.
127+
}
128+
_, err := run("find", ".",
129+
"-type", "f",
130+
"-not", "-path", "./sdk/*",
131+
"-not", "-path", "./upstream/*",
132+
"-not", "-path", "./.git/*",
133+
"-not", "-path", "./.pulumi/*",
134+
"-exec", "sed", "-i.bak",
135+
fmt.Sprintf("s/%s/%s/g",
136+
strings.Replace(oldImport, "/", `\/`, -1),
137+
strings.Replace(newImport, "/", `\/`, -1),
138+
), "{}", ";")
139+
if err != nil {
140+
return fmt.Errorf("rewriting %q to %q: %w", oldImport, newImport, err)
141+
}
142+
_, err = run("find", ".", "-name", "*.bak", "-exec", "rm", "{}", "+")
143+
if err != nil {
144+
return fmt.Errorf("cleaning up: %w", err)
145+
}
146+
return nil
147+
148+
}
149+
if err := rewriteImport(oldImport, newImport); err != nil {
150+
return err
151+
}
152+
if err := rewriteImport(
153+
strings.Replace(oldImport, "provider", "examples", 1),
154+
strings.Replace(newImport, "provider", "examples", 1),
155+
); err != nil {
156+
return err
157+
}
158+
if err := rewriteImport(
159+
strings.Replace(oldImport, "provider", "tests", 1),
160+
strings.Replace(newImport, "provider", "tests", 1),
161+
); err != nil {
162+
return err
163+
}
164+
165+
// Tidy up.
166+
_, err = run("go", "mod", "tidy")
167+
if err != nil {
168+
return fmt.Errorf("tidying up: %w", err)
169+
}
170+
171+
return nil
172+
173+
}
174+
175+
// The types below are for loading the module as JSON and are copied from `go
176+
// help mod edit`.
177+
178+
type module struct {
179+
Path string
180+
Version string
181+
}
182+
183+
type gomod struct {
184+
Module modpath
185+
Go string
186+
Toolchain string
187+
Require []requirement
188+
Exclude []module
189+
Replace []replace
190+
Retract []retract
191+
}
192+
193+
type modpath struct {
194+
Path string
195+
Deprecated string
196+
}
197+
198+
type requirement struct {
199+
Path string
200+
Version string
201+
Indirect bool
202+
}
203+
204+
type replace struct {
205+
Old module
206+
New module
207+
}
208+
209+
type retract struct {
210+
Low string
211+
High string
212+
Rationale string
213+
}

provider-ci/internal/pkg/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Migration interface {
1414

1515
func Migrate(templateName, outDir string) error {
1616
migrations := []Migration{
17+
consolidateModules{},
1718
fixupBridgeImports{},
1819
removeExplicitSDKDependency{},
1920
ignoreMakeDir{},
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package migrations
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestConsolidateModules(t *testing.T) {
14+
tests, err := os.ReadDir("testdata/modules")
15+
require.NoError(t, err)
16+
17+
for _, tt := range tests {
18+
t.Run(tt.Name(), func(t *testing.T) {
19+
if !tt.IsDir() {
20+
return
21+
}
22+
23+
tmp := t.TempDir()
24+
25+
// Copy GIVEN to a temp directory so we can mutate it.
26+
given := filepath.Join("testdata/modules", tt.Name(), "GIVEN")
27+
err = os.CopyFS(tmp, os.DirFS(given))
28+
29+
// We need to operate on a git repo, so initialize one.
30+
out, err := exec.Command("git", "init", tmp).CombinedOutput()
31+
require.NoError(t, err, string(out))
32+
out, err = exec.Command("git", "-C", tmp, "add", ".").CombinedOutput()
33+
require.NoError(t, err, string(out))
34+
out, err = exec.Command("git", "-C", tmp,
35+
"-c", "user.name=pulumi-bot", "-c", "[email protected]",
36+
"commit", "-m", "Initial commit").CombinedOutput()
37+
require.NoError(t, err, string(out))
38+
39+
// Do the migration.
40+
m := consolidateModules{}
41+
err = m.Migrate("", tmp)
42+
require.NoError(t, err)
43+
44+
// Make sure we got the expected output.
45+
want := filepath.Join("testdata/modules", tt.Name(), "WANT")
46+
assertDirectoryContains(t, want, tmp)
47+
assertDirectoryContains(t, tmp, want)
48+
})
49+
}
50+
}
51+
52+
// assertDirectoryContains asserts that dir1 contains all of the files in dir2
53+
// with exactly the same context. The .git directory is ignored.
54+
func assertDirectoryContains(t *testing.T, dir1, dir2 string) {
55+
t.Helper()
56+
57+
entries, err := os.ReadDir(dir2)
58+
require.NoError(t, err)
59+
60+
for _, entry := range entries {
61+
if entry.Name() == ".git" {
62+
continue
63+
}
64+
65+
stat, err := os.Stat(filepath.Join(dir1, entry.Name()))
66+
assert.NoError(t, err)
67+
assert.Equal(t, entry.IsDir(), stat.IsDir())
68+
69+
subPath1 := filepath.Join(dir1, entry.Name())
70+
subPath2 := filepath.Join(dir2, entry.Name())
71+
72+
if entry.IsDir() {
73+
assertDirectoryContains(t, subPath1, subPath2)
74+
continue
75+
}
76+
77+
content1, err := os.ReadFile(subPath1)
78+
assert.NoError(t, err)
79+
80+
content2, err := os.ReadFile(subPath2)
81+
assert.NoError(t, err)
82+
83+
assert.Equal(t, string(content1), string(content2), subPath1)
84+
}
85+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package examples
2+
3+
import (
4+
_ "github.com/pulumi/pulumi-foo/examples/some-package"
5+
_ "github.com/pulumi/pulumi-foo/provider"
6+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module github.com/pulumi/pulumi-foo/examples
2+
3+
go 1.22.7
4+
5+
replace (
6+
github.com/pulumi/pulumi-foo/provider => ../provider
7+
github.com/terraform-providers/terraform-provider-foo/shim => ../provider/shim
8+
)
9+
10+
require github.com/pulumi/pulumi-foo/provider v1.0.0-20230306191832-8c7659ab0229
11+
12+
require github.com/terraform-providers/terraform-provider-foo/shim v0.0.0 // indirect

provider-ci/internal/pkg/migrations/testdata/modules/v1/GIVEN/examples/go.sum

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package bar

0 commit comments

Comments
 (0)