Skip to content

Commit 77ba0fa

Browse files
authored
feat: add repo mode (#4)
1 parent 24b93e7 commit 77ba0fa

File tree

6 files changed

+219
-36
lines changed

6 files changed

+219
-36
lines changed

go.mod

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa55
1010

1111
require (
1212
github.com/GoogleContainerTools/kaniko v1.9.2
13-
github.com/coder/envbuilder v1.0.0-rc.0.0.20240803183847-6afe89e6950e
13+
github.com/coder/envbuilder v1.0.0-rc.0.0.20240805094524-c1f9917dfb61
1414
github.com/docker/docker v26.1.4+incompatible
15+
github.com/gliderlabs/ssh v0.3.7
1516
github.com/go-git/go-billy/v5 v5.5.0
17+
github.com/go-git/go-git/v5 v5.12.0
1618
github.com/google/go-containerregistry v0.19.1
1719
github.com/hashicorp/terraform-plugin-docs v0.19.4
1820
github.com/hashicorp/terraform-plugin-framework v1.10.0
@@ -58,6 +60,7 @@ require (
5860
github.com/agext/levenshtein v1.2.3 // indirect
5961
github.com/akutz/memconn v0.1.0 // indirect
6062
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
63+
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
6164
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
6265
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect
6366
github.com/aws/aws-sdk-go-v2 v1.30.0 // indirect
@@ -128,7 +131,6 @@ require (
128131
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
129132
github.com/go-chi/chi/v5 v5.0.10 // indirect
130133
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
131-
github.com/go-git/go-git/v5 v5.12.0 // indirect
132134
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
133135
github.com/go-logr/logr v1.4.1 // indirect
134136
github.com/go-logr/stdr v1.2.2 // indirect

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC
186186
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
187187
github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0=
188188
github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo=
189-
github.com/coder/envbuilder v1.0.0-rc.0.0.20240803183847-6afe89e6950e h1:gchZb6E2C5giRJwS2wPjbwHfxle4rJX7NqHCpN1XaT0=
190-
github.com/coder/envbuilder v1.0.0-rc.0.0.20240803183847-6afe89e6950e/go.mod h1:SCpGkbd04qsTIHUYRWEJMgt4R+uK+q4lGnOhEyTorjU=
189+
github.com/coder/envbuilder v1.0.0-rc.0.0.20240805094524-c1f9917dfb61 h1:SPOT1R13rgJie9l+VUsqd4TiqzSeGD2AmEv8wzmAcDE=
190+
github.com/coder/envbuilder v1.0.0-rc.0.0.20240805094524-c1f9917dfb61/go.mod h1:SCpGkbd04qsTIHUYRWEJMgt4R+uK+q4lGnOhEyTorjU=
191191
github.com/coder/kaniko v0.0.0-20240803153527-10d1800455b9 h1:d01T5YbPN1yc1mXjIXG59YcQQoT/9idvqFErjWHfsZ4=
192192
github.com/coder/kaniko v0.0.0-20240803153527-10d1800455b9/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM=
193193
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=

internal/provider/cached_image_data_source.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,15 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
295295
// This may require changing this to be a resource instead of a data source.
296296
opts := eboptions.Options{
297297
// These options are always required
298-
CacheRepo: data.CacheRepo.ValueString(),
299-
Filesystem: osfs.New("/"),
300-
ForceSafe: false, // This should never be set to true, as this may be running outside of a container!
301-
GetCachedImage: true, // always!
302-
Logger: tfLogFunc(ctx),
303-
Verbose: data.Verbose.ValueBool(),
304-
WorkspaceFolder: workspaceFolder,
298+
CacheRepo: data.CacheRepo.ValueString(),
299+
Filesystem: osfs.New("/"),
300+
ForceSafe: false, // This should never be set to true, as this may be running outside of a container!
301+
GetCachedImage: true, // always!
302+
Logger: tfLogFunc(ctx),
303+
Verbose: data.Verbose.ValueBool(),
304+
WorkspaceFolder: workspaceFolder,
305+
RemoteRepoBuildMode: true,
306+
RemoteRepoDir: filepath.Join(tmpDir, "repo"), // Hidden option used by this provider.
305307

306308
// Options related to compiling the devcontainer
307309
BuildContextPath: data.BuildContextPath.ValueString(),

internal/provider/cached_image_data_source_test.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,26 @@ import (
1818
func TestAccCachedImageDataSource(t *testing.T) {
1919
t.Run("Found", func(t *testing.T) {
2020
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
21-
t.Cleanup(cancel)
21+
defer cancel()
2222
files := map[string]string{
2323
".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`,
2424
".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
2525
RUN apt-get update && apt-get install -y cowsay`,
2626
}
27-
deps := setup(t, files)
27+
28+
deps := setup(ctx, t, files)
2829
seedCache(ctx, t, deps)
2930
tfCfg := fmt.Sprintf(`data "envbuilder_cached_image" "test" {
3031
builder_image = %q
3132
workspace_folder = %q
3233
git_url = %q
34+
git_ssh_private_key_path = %q
3335
extra_env = {
3436
"FOO" : "bar"
3537
}
3638
cache_repo = %q
3739
verbose = true
38-
}`, deps.BuilderImage, deps.RepoDir, deps.RepoDir, deps.CacheRepo)
40+
}`, deps.BuilderImage, "/workspace", deps.Repo.URL, deps.Repo.Key, deps.CacheRepo)
3941
resource.Test(t, resource.TestCase{
4042
PreCheck: func() { testAccPreCheck(t) },
4143
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
@@ -46,7 +48,7 @@ func TestAccCachedImageDataSource(t *testing.T) {
4648
// Inputs should still be present.
4749
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
4850
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "extra_env.FOO", "bar"),
49-
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.RepoDir),
51+
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.Repo.URL),
5052
// Should be empty
5153
resource.TestCheckNoResourceAttr("data.envbuilder_cached_image.test", "git_username"),
5254
resource.TestCheckNoResourceAttr("data.envbuilder_cached_image.test", "git_password"),
@@ -78,23 +80,26 @@ func TestAccCachedImageDataSource(t *testing.T) {
7880
})
7981

8082
t.Run("NotFound", func(t *testing.T) {
83+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
84+
defer cancel()
8185
files := map[string]string{
8286
".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`,
8387
".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
8488
RUN apt-get update && apt-get install -y cowsay`,
8589
}
86-
deps := setup(t, files)
90+
deps := setup(ctx, t, files)
8791
// We do not seed the cache.
8892
tfCfg := fmt.Sprintf(`data "envbuilder_cached_image" "test" {
8993
builder_image = %q
9094
workspace_folder = %q
9195
git_url = %q
96+
git_ssh_private_key_path = %q
9297
extra_env = {
9398
"FOO" : "bar"
9499
}
95100
cache_repo = %q
96101
verbose = true
97-
}`, deps.BuilderImage, deps.RepoDir, deps.RepoDir, deps.CacheRepo)
102+
}`, deps.BuilderImage, "/workspace", deps.Repo.URL, deps.Repo.Key, deps.CacheRepo)
98103
resource.Test(t, resource.TestCase{
99104
PreCheck: func() { testAccPreCheck(t) },
100105
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
@@ -105,7 +110,7 @@ func TestAccCachedImageDataSource(t *testing.T) {
105110
// Inputs should still be present.
106111
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
107112
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "extra_env.FOO", "bar"),
108-
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.RepoDir),
113+
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.Repo.URL),
109114
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "exists", "false"),
110115
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "image", deps.BuilderImage),
111116
// Should be empty

internal/provider/git_test.go

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"net"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/gliderlabs/ssh"
14+
"github.com/go-git/go-git/v5"
15+
"github.com/go-git/go-git/v5/plumbing"
16+
"github.com/go-git/go-git/v5/plumbing/object"
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
// nolint:gosec // Throw-away key for testing. DO NOT REUSE.
22+
const (
23+
testSSHKey = `-----BEGIN OPENSSH PRIVATE KEY-----
24+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
25+
QyNTUxOQAAACCtxz9h0yXzi/HqZBpSkA2xFo28v5W8O4HimI0ZzNpQkwAAAKhv/+X2b//l
26+
9gAAAAtzc2gtZWQyNTUxOQAAACCtxz9h0yXzi/HqZBpSkA2xFo28v5W8O4HimI0ZzNpQkw
27+
AAAED/G0HuohvSa8q6NzkZ+wRPW0PhPpo9Th8fvcBQDaxCia3HP2HTJfOL8epkGlKQDbEW
28+
jby/lbw7geKYjRnM2lCTAAAAInRlcnJhZm9ybS1wcm92aWRlci1lbnZidWlsZGVyLXRlc3
29+
QBAgM=
30+
-----END OPENSSH PRIVATE KEY-----`
31+
testSSHPubKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK3HP2HTJfOL8epkGlKQDbEWjby/lbw7geKYjRnM2lCT terraform-provider-envbuilder-test`
32+
)
33+
34+
func setupGitRepo(t testing.TB, files map[string]string) string {
35+
t.Helper()
36+
37+
dir := filepath.Join(t.TempDir(), "repo")
38+
39+
writeFiles(t, dir, files)
40+
41+
repo, err := git.PlainInitWithOptions(dir, &git.PlainInitOptions{
42+
InitOptions: git.InitOptions{
43+
DefaultBranch: plumbing.ReferenceName("refs/heads/main"),
44+
},
45+
})
46+
require.NoError(t, err, "init git repo")
47+
wt, err := repo.Worktree()
48+
require.NoError(t, err, "get worktree")
49+
_, err = wt.Add(".")
50+
require.NoError(t, err, "add files")
51+
_, err = wt.Commit("initial commit", &git.CommitOptions{
52+
Author: &object.Signature{
53+
Name: "test",
54+
55+
},
56+
})
57+
require.NoError(t, err, "commit files")
58+
t.Logf("initialized git repo at %s", dir)
59+
60+
return dir
61+
}
62+
63+
func writeFiles(t testing.TB, destPath string, files map[string]string) {
64+
t.Helper()
65+
66+
err := os.MkdirAll(destPath, 0o755)
67+
require.NoError(t, err, "create dest path")
68+
69+
for relPath, content := range files {
70+
absPath := filepath.Join(destPath, relPath)
71+
d := filepath.Dir(absPath)
72+
bs := []byte(content)
73+
require.NoError(t, os.MkdirAll(d, 0o755))
74+
require.NoError(t, os.WriteFile(absPath, bs, 0o644))
75+
t.Logf("wrote %d bytes to %s", len(bs), absPath)
76+
}
77+
}
78+
79+
type testGitRepoSSH struct {
80+
Dir string
81+
URL string
82+
Key string
83+
}
84+
85+
func serveGitRepoSSH(ctx context.Context, t testing.TB, dir string) testGitRepoSSH {
86+
t.Helper()
87+
88+
sshDir := filepath.Join(t.TempDir(), "ssh")
89+
require.NoError(t, os.Mkdir(sshDir, 0o700))
90+
91+
keyPath := filepath.Join(sshDir, "id_ed25519")
92+
require.NoError(t, os.WriteFile(keyPath, []byte(testSSHKey), 0o600))
93+
94+
// Start SSH server
95+
addr := startSSHServer(ctx, t)
96+
97+
// Serve git repo
98+
repoURL := "ssh://" + addr + dir
99+
return testGitRepoSSH{
100+
Dir: dir,
101+
URL: repoURL,
102+
Key: keyPath,
103+
}
104+
}
105+
106+
func startSSHServer(ctx context.Context, t testing.TB) string {
107+
t.Helper()
108+
109+
s := &ssh.Server{
110+
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {
111+
return true // Allow all keys.
112+
},
113+
Handler: func(s ssh.Session) {
114+
t.Logf("session started: %s", s.RawCommand())
115+
116+
args := s.Command()
117+
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
118+
119+
in, err := cmd.StdinPipe()
120+
assert.NoError(t, err, "stdin pipe")
121+
out, err := cmd.StdoutPipe()
122+
assert.NoError(t, err, "stdout pipe")
123+
err = cmd.Start()
124+
if err != nil {
125+
t.Logf("command failed: %s", err)
126+
return
127+
}
128+
t.Cleanup(func() {
129+
_ = in.Close()
130+
_ = out.Close()
131+
_ = cmd.Process.Kill()
132+
})
133+
134+
go func() {
135+
_, _ = io.Copy(in, s)
136+
_ = in.Close()
137+
}()
138+
go func() {
139+
_, _ = io.Copy(s, out)
140+
_ = out.Close()
141+
_ = s.CloseWrite()
142+
}()
143+
err = cmd.Wait()
144+
if err != nil {
145+
t.Logf("command failed: %s", err)
146+
}
147+
148+
t.Logf("session ended: %s", s.RawCommand())
149+
150+
err = s.Exit(cmd.ProcessState.ExitCode())
151+
if err != nil {
152+
t.Logf("session exit failed: %s", err)
153+
}
154+
},
155+
}
156+
157+
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "localhost:0")
158+
require.NoError(t, err, "listen")
159+
160+
go func() {
161+
err := s.Serve(ln)
162+
if !errors.Is(err, ssh.ErrServerClosed) {
163+
require.NoError(t, err)
164+
}
165+
}()
166+
t.Cleanup(func() {
167+
_ = s.Close()
168+
_ = ln.Close()
169+
})
170+
171+
return ln.Addr().String()
172+
}

0 commit comments

Comments
 (0)