terraform-provider-envbuilder/internal/provider/provider_test.go
Cian Johnston c9e7cb8178
implement first pass at cached image data source ()
implements envbuilder_cached_image data source
2024-08-01 21:26:44 +01:00

167 lines
5.1 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"bufio"
"context"
"io"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/mafredri/terraform-provider-envbuilder/testutil/registrytest"
"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")()),
}
func testAccPreCheck(t *testing.T) {
// You can add code here to run prior to any test case execution, for example assertions
// about the appropriate environment variables being set are common to see in a pre-check
// function.
}
type testDependencies struct {
BuilderImage string
RepoDir string
CacheRepo string
}
func setup(t testing.TB, files map[string]string) testDependencies {
t.Helper()
envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "ghcr.io/coder/envbuilder-preview")
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.
repoDir := t.TempDir()
regDir := t.TempDir()
reg := registrytest.New(t, regDir)
writeFiles(t, files, repoDir)
return testDependencies{
BuilderImage: envbuilderImageRef,
CacheRepo: reg + "/test",
RepoDir: repoDir,
}
}
func seedCache(ctx context.Context, t testing.TB, deps testDependencies) {
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,
Env: []string{
"ENVBUILDER_CACHE_REPO=" + deps.CacheRepo,
"ENVBUILDER_DEVCONTAINER_DIR=" + deps.RepoDir,
"ENVBUILDER_EXIT_ON_BUILD_FAILURE=true",
"ENVBUILDER_INIT_SCRIPT=exit",
// FIXME: Enabling this options causes envbuilder to add its binary to the image under the path
// /.envbuilder/bin/envbuilder. This file will have ownership root:root and permissions 0o755.
// Because of this, t.Cleanup() will be unable to delete the temp dir, causing the test to fail.
// "ENVBUILDER_PUSH_IMAGE=true",
},
Labels: map[string]string{
testContainerLabel: "true",
}}, &container.HostConfig{
NetworkMode: container.NetworkMode("host"),
Binds: []string{deps.RepoDir + ":" + deps.RepoDir},
}, nil, nil, "")
require.NoError(t, err, "failed to run envbuilder to seed cache")
t.Cleanup(func() {
_ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
RemoveVolumes: true,
Force: true,
})
})
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 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()
t.Logf("ensuring image %q", ref)
images, err := cli.ImageList(ctx, image.ListOptions{})
require.NoError(t, err, "list images")
for _, img := range images {
if slices.Contains(img.RepoTags, ref) {
t.Logf("image %q found locally, not pulling", ref)
return
}
}
t.Logf("image %s not found locally, attempting to pull", ref)
resp, err := cli.ImagePull(ctx, ref, image.PullOptions{})
require.NoError(t, err)
_, err = io.ReadAll(resp)
require.NoError(t, err)
}