feat: add repo mode ()

This commit is contained in:
Mathias Fredriksson 2024-08-05 17:35:12 +03:00 committed by GitHub
commit 77ba0fab6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 219 additions and 36 deletions

6
go.mod
View file

@ -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
@ -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
@ -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

4
go.sum
View file

@ -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=

View file

@ -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(),

View file

@ -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,
@ -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"),
@ -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,
@ -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

View file

@ -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: "test@coder.com",
},
})
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() {
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()
}

View file

@ -8,7 +8,6 @@ import (
"context"
"io"
"os"
"path/filepath"
"slices"
"strings"
"testing"
@ -43,11 +42,11 @@ func testAccPreCheck(t *testing.T) {
type testDependencies struct {
BuilderImage string
RepoDir string
CacheRepo string
Repo testGitRepoSSH
}
func setup(t testing.TB, files map[string]string) testDependencies {
func setup(ctx context.Context, t testing.TB, files map[string]string) testDependencies {
t.Helper()
envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "ghcr.io/coder/envbuilder-preview")
@ -56,22 +55,31 @@ func setup(t testing.TB, files map[string]string) testDependencies {
// TODO: envbuilder creates /.envbuilder/bin/envbuilder owned by root:root which we are unable to clean up.
// This causes tests to fail.
repoDir := t.TempDir()
regDir := t.TempDir()
reg := registrytest.New(t, regDir)
writeFiles(t, files, repoDir)
repoDir := setupGitRepo(t, files)
gitRepo := serveGitRepoSSH(ctx, t, repoDir)
return testDependencies{
BuilderImage: envbuilderImageRef,
CacheRepo: reg + "/test",
RepoDir: repoDir,
Repo: gitRepo,
}
}
func seedCache(ctx context.Context, t testing.TB, deps testDependencies) {
t.Helper()
t.Logf("seeding cache with %s", deps.CacheRepo)
defer t.Logf("finished seeding cache with %s", deps.CacheRepo)
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
require.NoError(t, err, "init docker client")
t.Cleanup(func() { _ = cli.Close() })
ensureImage(ctx, t, cli, deps.BuilderImage)
// Run envbuilder using this dir as a local layer cache
ctr, err := cli.ContainerCreate(ctx, &container.Config{
Image: deps.BuilderImage,
@ -81,14 +89,19 @@ func seedCache(ctx context.Context, t testing.TB, deps testDependencies) {
"ENVBUILDER_INIT_SCRIPT=exit",
"ENVBUILDER_PUSH_IMAGE=true",
"ENVBUILDER_VERBOSE=true",
"ENVBUILDER_GIT_URL=" + deps.Repo.URL,
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH=/id_ed25519",
},
Labels: map[string]string{
testContainerLabel: "true",
},
}, &container.HostConfig{
NetworkMode: container.NetworkMode("host"),
Binds: []string{deps.RepoDir + ":" + "/workspaces/empty"},
Binds: []string{
deps.Repo.Key + ":/id_ed25519",
},
}, nil, nil, "")
require.NoError(t, err, "failed to run envbuilder to seed cache")
t.Cleanup(func() {
_ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
@ -133,17 +146,6 @@ func getEnvOrDefault(env, defVal string) string {
return defVal
}
func writeFiles(t testing.TB, files map[string]string, destPath string) {
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)
}
}
func ensureImage(ctx context.Context, t testing.TB, cli *client.Client, ref string) {
t.Helper()