Skip to content

feat: add repo mode #4

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

Merged
merged 10 commits into from
Aug 5, 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
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa55

require (
github.com/GoogleContainerTools/kaniko v1.9.2
github.com/coder/envbuilder v1.0.0-rc.0.0.20240803183847-6afe89e6950e
github.com/coder/envbuilder v1.0.0-rc.0.0.20240805094524-c1f9917dfb61
github.com/docker/docker v26.1.4+incompatible
github.com/gliderlabs/ssh v0.3.7
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-containerregistry v0.19.1
github.com/hashicorp/terraform-plugin-docs v0.19.4
github.com/hashicorp/terraform-plugin-framework v1.10.0
Expand Down Expand Up @@ -58,6 +60,7 @@ require (
github.com/agext/levenshtein v1.2.3 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect
github.com/aws/aws-sdk-go-v2 v1.30.0 // indirect
Expand Down Expand Up @@ -128,7 +131,6 @@ require (
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0=
github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo=
github.com/coder/envbuilder v1.0.0-rc.0.0.20240803183847-6afe89e6950e h1:gchZb6E2C5giRJwS2wPjbwHfxle4rJX7NqHCpN1XaT0=
github.com/coder/envbuilder v1.0.0-rc.0.0.20240803183847-6afe89e6950e/go.mod h1:SCpGkbd04qsTIHUYRWEJMgt4R+uK+q4lGnOhEyTorjU=
github.com/coder/envbuilder v1.0.0-rc.0.0.20240805094524-c1f9917dfb61 h1:SPOT1R13rgJie9l+VUsqd4TiqzSeGD2AmEv8wzmAcDE=
github.com/coder/envbuilder v1.0.0-rc.0.0.20240805094524-c1f9917dfb61/go.mod h1:SCpGkbd04qsTIHUYRWEJMgt4R+uK+q4lGnOhEyTorjU=
github.com/coder/kaniko v0.0.0-20240803153527-10d1800455b9 h1:d01T5YbPN1yc1mXjIXG59YcQQoT/9idvqFErjWHfsZ4=
github.com/coder/kaniko v0.0.0-20240803153527-10d1800455b9/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
Expand Down
16 changes: 9 additions & 7 deletions internal/provider/cached_image_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,15 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
// This may require changing this to be a resource instead of a data source.
opts := eboptions.Options{
// These options are always required
CacheRepo: data.CacheRepo.ValueString(),
Filesystem: osfs.New("/"),
ForceSafe: false, // This should never be set to true, as this may be running outside of a container!
GetCachedImage: true, // always!
Logger: tfLogFunc(ctx),
Verbose: data.Verbose.ValueBool(),
WorkspaceFolder: workspaceFolder,
CacheRepo: data.CacheRepo.ValueString(),
Filesystem: osfs.New("/"),
ForceSafe: false, // This should never be set to true, as this may be running outside of a container!
GetCachedImage: true, // always!
Logger: tfLogFunc(ctx),
Verbose: data.Verbose.ValueBool(),
WorkspaceFolder: workspaceFolder,
RemoteRepoBuildMode: true,
RemoteRepoDir: filepath.Join(tmpDir, "repo"), // Hidden option used by this provider.

// Options related to compiling the devcontainer
BuildContextPath: data.BuildContextPath.ValueString(),
Expand Down
19 changes: 12 additions & 7 deletions internal/provider/cached_image_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,26 @@ import (
func TestAccCachedImageDataSource(t *testing.T) {
t.Run("Found", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
defer cancel()
files := map[string]string{
".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`,
".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
RUN apt-get update && apt-get install -y cowsay`,
}
deps := setup(t, files)

deps := setup(ctx, t, files)
seedCache(ctx, t, deps)
tfCfg := fmt.Sprintf(`data "envbuilder_cached_image" "test" {
builder_image = %q
workspace_folder = %q
git_url = %q
git_ssh_private_key_path = %q
extra_env = {
"FOO" : "bar"
}
cache_repo = %q
verbose = true
}`, deps.BuilderImage, deps.RepoDir, deps.RepoDir, deps.CacheRepo)
}`, deps.BuilderImage, "/workspace", deps.Repo.URL, deps.Repo.Key, deps.CacheRepo)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Expand All @@ -46,7 +48,7 @@ func TestAccCachedImageDataSource(t *testing.T) {
// Inputs should still be present.
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "extra_env.FOO", "bar"),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.RepoDir),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.Repo.URL),
// Should be empty
resource.TestCheckNoResourceAttr("data.envbuilder_cached_image.test", "git_username"),
resource.TestCheckNoResourceAttr("data.envbuilder_cached_image.test", "git_password"),
Expand Down Expand Up @@ -78,23 +80,26 @@ func TestAccCachedImageDataSource(t *testing.T) {
})

t.Run("NotFound", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
files := map[string]string{
".devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`,
".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
RUN apt-get update && apt-get install -y cowsay`,
}
deps := setup(t, files)
deps := setup(ctx, t, files)
// We do not seed the cache.
tfCfg := fmt.Sprintf(`data "envbuilder_cached_image" "test" {
builder_image = %q
workspace_folder = %q
git_url = %q
git_ssh_private_key_path = %q
extra_env = {
"FOO" : "bar"
}
cache_repo = %q
verbose = true
}`, deps.BuilderImage, deps.RepoDir, deps.RepoDir, deps.CacheRepo)
}`, deps.BuilderImage, "/workspace", deps.Repo.URL, deps.Repo.Key, deps.CacheRepo)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Expand All @@ -105,7 +110,7 @@ func TestAccCachedImageDataSource(t *testing.T) {
// Inputs should still be present.
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "extra_env.FOO", "bar"),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.RepoDir),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "git_url", deps.Repo.URL),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "exists", "false"),
resource.TestCheckResourceAttr("data.envbuilder_cached_image.test", "image", deps.BuilderImage),
// Should be empty
Expand Down
172 changes: 172 additions & 0 deletions internal/provider/git_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package provider

import (
"context"
"errors"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/gliderlabs/ssh"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// nolint:gosec // Throw-away key for testing. DO NOT REUSE.
const (
testSSHKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCtxz9h0yXzi/HqZBpSkA2xFo28v5W8O4HimI0ZzNpQkwAAAKhv/+X2b//l
9gAAAAtzc2gtZWQyNTUxOQAAACCtxz9h0yXzi/HqZBpSkA2xFo28v5W8O4HimI0ZzNpQkw
AAAED/G0HuohvSa8q6NzkZ+wRPW0PhPpo9Th8fvcBQDaxCia3HP2HTJfOL8epkGlKQDbEW
jby/lbw7geKYjRnM2lCTAAAAInRlcnJhZm9ybS1wcm92aWRlci1lbnZidWlsZGVyLXRlc3
QBAgM=
-----END OPENSSH PRIVATE KEY-----`
testSSHPubKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK3HP2HTJfOL8epkGlKQDbEWjby/lbw7geKYjRnM2lCT terraform-provider-envbuilder-test`
)

func setupGitRepo(t testing.TB, files map[string]string) string {
t.Helper()

dir := filepath.Join(t.TempDir(), "repo")

writeFiles(t, dir, files)

repo, err := git.PlainInitWithOptions(dir, &git.PlainInitOptions{
InitOptions: git.InitOptions{
DefaultBranch: plumbing.ReferenceName("refs/heads/main"),
},
})
require.NoError(t, err, "init git repo")
wt, err := repo.Worktree()
require.NoError(t, err, "get worktree")
_, err = wt.Add(".")
require.NoError(t, err, "add files")
_, err = wt.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "test",
Email: "[email protected]",
},
})
require.NoError(t, err, "commit files")
t.Logf("initialized git repo at %s", dir)

return dir
}

func writeFiles(t testing.TB, destPath string, files map[string]string) {
t.Helper()

err := os.MkdirAll(destPath, 0o755)
require.NoError(t, err, "create dest path")

for relPath, content := range files {
absPath := filepath.Join(destPath, relPath)
d := filepath.Dir(absPath)
bs := []byte(content)
require.NoError(t, os.MkdirAll(d, 0o755))
require.NoError(t, os.WriteFile(absPath, bs, 0o644))
t.Logf("wrote %d bytes to %s", len(bs), absPath)
}
}

type testGitRepoSSH struct {
Dir string
URL string
Key string
}

func serveGitRepoSSH(ctx context.Context, t testing.TB, dir string) testGitRepoSSH {
t.Helper()

sshDir := filepath.Join(t.TempDir(), "ssh")
require.NoError(t, os.Mkdir(sshDir, 0o700))

keyPath := filepath.Join(sshDir, "id_ed25519")
require.NoError(t, os.WriteFile(keyPath, []byte(testSSHKey), 0o600))

// Start SSH server
addr := startSSHServer(ctx, t)

// Serve git repo
repoURL := "ssh://" + addr + dir
return testGitRepoSSH{
Dir: dir,
URL: repoURL,
Key: keyPath,
}
}

func startSSHServer(ctx context.Context, t testing.TB) string {
t.Helper()

s := &ssh.Server{
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {
return true // Allow all keys.
},
Handler: func(s ssh.Session) {
t.Logf("session started: %s", s.RawCommand())

args := s.Command()
cmd := exec.CommandContext(ctx, args[0], args[1:]...)

in, err := cmd.StdinPipe()
assert.NoError(t, err, "stdin pipe")
out, err := cmd.StdoutPipe()
assert.NoError(t, err, "stdout pipe")
err = cmd.Start()
if err != nil {
t.Logf("command failed: %s", err)
return
}
t.Cleanup(func() {
_ = in.Close()
_ = out.Close()
_ = cmd.Process.Kill()
})

go func() {
_, _ = io.Copy(in, s)
_ = in.Close()
}()
go func() {
_, _ = io.Copy(s, out)
_ = out.Close()
_ = s.CloseWrite()
}()
err = cmd.Wait()
if err != nil {
t.Logf("command failed: %s", err)
}

t.Logf("session ended: %s", s.RawCommand())

err = s.Exit(cmd.ProcessState.ExitCode())
if err != nil {
t.Logf("session exit failed: %s", err)
}
},
}

ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "localhost:0")
require.NoError(t, err, "listen")

go func() {
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 to wait for this goroutine to exit?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not worth the extra complexity here I think, we're not using leak detection (yet).

err := s.Serve(ln)
if !errors.Is(err, ssh.ErrServerClosed) {
require.NoError(t, err)
}
}()
t.Cleanup(func() {
_ = s.Close()
_ = ln.Close()
})

return ln.Addr().String()
}
Loading