mirror of
https://github.com/coder/terraform-provider-envbuilder.git
synced 2025-08-19 17:47:10 +00:00
Relates to #43 Our previous logic did not pass options from extra_env to envbuilder.RunCacheProbe. This fixes the logic and adds more comprehensive tests around the overriding logic. Future commits will refactor this logic some more.
228 lines
6.3 KiB
Go
228 lines
6.3 KiB
Go
package provider
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
|
|
"github.com/coder/terraform-provider-envbuilder/testutil/registrytest"
|
|
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
|
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/client"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testContainerLabel = "terraform-provider-envbuilder-test"
|
|
)
|
|
|
|
// testAccProtoV6ProviderFactories are used to instantiate a provider during
|
|
// acceptance testing. The factory function will be invoked for every Terraform
|
|
// CLI command executed to create a provider server to which the CLI can
|
|
// reattach.
|
|
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
|
|
"envbuilder": providerserver.NewProtocol6WithError(New("test")()),
|
|
}
|
|
|
|
// testDependencies contain information about stuff the test depends on.
|
|
type testDependencies struct {
|
|
BuilderImage string
|
|
CacheRepo string
|
|
ExtraEnv map[string]string
|
|
Repo testGitRepoSSH
|
|
}
|
|
|
|
// Config generates a valid Terraform config file from the dependencies.
|
|
func (d *testDependencies) Config(t testing.TB) string {
|
|
t.Helper()
|
|
|
|
tpl := `provider envbuilder {}
|
|
resource "envbuilder_cached_image" "test" {
|
|
builder_image = {{ quote .BuilderImage }}
|
|
cache_repo = {{ quote .CacheRepo }}
|
|
git_url = {{ quote .Repo.URL }}
|
|
extra_env = {
|
|
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": {{ quote .Repo.Key }}
|
|
"ENVBUILDER_VERBOSE": true
|
|
{{ range $k, $v := .ExtraEnv }}
|
|
{{ quote $k }}: {{ quote $v }}
|
|
{{ end }}
|
|
}
|
|
}`
|
|
|
|
fm := template.FuncMap{"quote": quote}
|
|
var sb strings.Builder
|
|
tmpl, err := template.New("envbuilder_cached_image").Funcs(fm).Parse(tpl)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tmpl.Execute(&sb, d))
|
|
return sb.String()
|
|
}
|
|
|
|
func quote(s string) string {
|
|
return fmt.Sprintf("%q", s)
|
|
}
|
|
|
|
func setup(ctx context.Context, t testing.TB, extraEnv, files map[string]string) testDependencies {
|
|
t.Helper()
|
|
|
|
envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "localhost:5000/envbuilder")
|
|
envbuilderVersion := getEnvOrDefault("ENVBUILDER_VERSION", "latest")
|
|
envbuilderImageRef := envbuilderImage + ":" + envbuilderVersion
|
|
|
|
// TODO: envbuilder creates /.envbuilder/bin/envbuilder owned by root:root which we are unable to clean up.
|
|
// This causes tests to fail.
|
|
regDir := t.TempDir()
|
|
reg := registrytest.New(t, regDir)
|
|
|
|
repoDir := setupGitRepo(t, files)
|
|
gitRepo := serveGitRepoSSH(ctx, t, repoDir)
|
|
|
|
return testDependencies{
|
|
BuilderImage: envbuilderImageRef,
|
|
CacheRepo: reg + "/test",
|
|
ExtraEnv: extraEnv,
|
|
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)
|
|
|
|
// Set up env for envbuilder
|
|
seedEnv := map[string]string{
|
|
"ENVBUILDER_CACHE_REPO": deps.CacheRepo,
|
|
"ENVBUILDER_EXIT_ON_BUILD_FAILURE": "true",
|
|
"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",
|
|
}
|
|
|
|
for k, v := range deps.ExtraEnv {
|
|
if !strings.HasPrefix(k, "ENVBUILDER_") {
|
|
continue
|
|
}
|
|
if _, ok := seedEnv[k]; ok {
|
|
continue
|
|
}
|
|
seedEnv[k] = v
|
|
}
|
|
|
|
seedDockerEnv := make([]string, 0)
|
|
for k, v := range seedEnv {
|
|
seedDockerEnv = append(seedDockerEnv, k+"="+v)
|
|
}
|
|
|
|
t.Logf("running envbuilder to seed cache with args: %v", seedDockerEnv)
|
|
|
|
// Run envbuilder using this dir as a local layer cache
|
|
ctr, err := cli.ContainerCreate(ctx, &container.Config{
|
|
Image: deps.BuilderImage,
|
|
Env: seedDockerEnv,
|
|
Labels: map[string]string{
|
|
testContainerLabel: "true",
|
|
},
|
|
}, &container.HostConfig{
|
|
NetworkMode: container.NetworkMode("host"),
|
|
Binds: []string{
|
|
deps.Repo.Key + ":/id_ed25519",
|
|
},
|
|
}, nil, nil, "")
|
|
|
|
require.NoError(t, err, "failed to run envbuilder to seed cache")
|
|
t.Cleanup(func() {
|
|
if err := cli.ContainerRemove(context.Background(), ctr.ID, container.RemoveOptions{
|
|
RemoveVolumes: true,
|
|
Force: true,
|
|
}); err != nil {
|
|
t.Errorf("removing container: %s", err.Error())
|
|
}
|
|
})
|
|
err = cli.ContainerStart(ctx, ctr.ID, container.StartOptions{})
|
|
require.NoError(t, err)
|
|
|
|
rawLogs, err := cli.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
|
|
ShowStdout: true,
|
|
ShowStderr: true,
|
|
Follow: true,
|
|
Timestamps: false,
|
|
})
|
|
require.NoError(t, err)
|
|
defer rawLogs.Close()
|
|
scanner := bufio.NewScanner(rawLogs)
|
|
SCANLOGS:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
require.Fail(t, "envbuilder did not finish running in time")
|
|
default:
|
|
if !scanner.Scan() {
|
|
require.Fail(t, "envbuilder did not run successfully")
|
|
}
|
|
log := scanner.Text()
|
|
t.Logf("envbuilder: %s", log)
|
|
if strings.Contains(log, "=== Running the init command") {
|
|
break SCANLOGS
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getEnvOrDefault(env, defVal string) string {
|
|
if val := os.Getenv(env); val != "" {
|
|
return val
|
|
}
|
|
return defVal
|
|
}
|
|
|
|
func ensureImage(ctx context.Context, t testing.TB, cli *client.Client, ref string) {
|
|
t.Helper()
|
|
|
|
t.Logf("ensuring image %q", ref)
|
|
images, err := cli.ImageList(ctx, image.ListOptions{})
|
|
require.NoError(t, err, "list images")
|
|
for _, img := range images {
|
|
if strings.HasSuffix(ref, ":latest") {
|
|
t.Logf("always pull latest")
|
|
break
|
|
} else if slices.Contains(img.RepoTags, ref) {
|
|
t.Logf("image %q found locally, not pulling", ref)
|
|
return
|
|
}
|
|
}
|
|
t.Logf("attempting to pull image %q", ref)
|
|
resp, err := cli.ImagePull(ctx, ref, image.PullOptions{})
|
|
require.NoError(t, err)
|
|
_, err = io.ReadAll(resp)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// quotedPrefix is a helper for asserting quoted strings.
|
|
func quotedPrefix(prefix string) func(string) error {
|
|
return func(val string) error {
|
|
trimmed := strings.Trim(val, `"`)
|
|
if !strings.HasPrefix(trimmed, prefix) {
|
|
return fmt.Errorf("expected value %q to have prefix %q", trimmed, prefix)
|
|
}
|
|
return nil
|
|
}
|
|
}
|