fix(internal/provider): correctly override from extra_env ()

Relates to 

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.
This commit is contained in:
Cian Johnston 2024-09-04 11:19:25 +01:00 committed by GitHub
parent e35030b39f
commit 23f2cf5f48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 737 additions and 163 deletions

View file

@ -16,12 +16,14 @@ import (
"github.com/coder/envbuilder/constants"
eblog "github.com/coder/envbuilder/log"
eboptions "github.com/coder/envbuilder/options"
"github.com/coder/serpent"
"github.com/go-git/go-billy/v5/osfs"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/uuid"
"github.com/spf13/pflag"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
@ -293,128 +295,246 @@ func (r *CachedImageResource) Configure(ctx context.Context, req resource.Config
r.client = client
}
// setComputedEnv sets data.Env and data.EnvMap based on the values of the
// other fields in the model.
func (data *CachedImageResourceModel) setComputedEnv(ctx context.Context) diag.Diagnostics {
env := make(map[string]string)
func optionsFromDataModel(data CachedImageResourceModel) (eboptions.Options, diag.Diagnostics) {
var diags diag.Diagnostics
var opts eboptions.Options
env["ENVBUILDER_CACHE_REPO"] = tfValueToString(data.CacheRepo)
env["ENVBUILDER_GIT_URL"] = tfValueToString(data.GitURL)
// Required options. Cannot be overridden by extra_env.
opts.CacheRepo = data.CacheRepo.ValueString()
opts.GitURL = data.GitURL.ValueString()
// Other options can be overridden by extra_env, with a warning.
// Keep track of which options are overridden.
overrides := make(map[string]struct{})
if !data.BaseImageCacheDir.IsNull() {
env["ENVBUILDER_BASE_IMAGE_CACHE_DIR"] = tfValueToString(data.BaseImageCacheDir)
overrides["ENVBUILDER_BASE_IMAGE_CACHE_DIR"] = struct{}{}
opts.BaseImageCacheDir = data.BaseImageCacheDir.ValueString()
}
if !data.BuildContextPath.IsNull() {
env["ENVBUILDER_BUILD_CONTEXT_PATH"] = tfValueToString(data.BuildContextPath)
overrides["ENVBUILDER_BUILD_CONTEXT_PATH"] = struct{}{}
opts.BuildContextPath = data.BuildContextPath.ValueString()
}
if !data.CacheTTLDays.IsNull() {
env["ENVBUILDER_CACHE_TTL_DAYS"] = tfValueToString(data.CacheTTLDays)
overrides["ENVBUILDER_CACHE_TTL_DAYS"] = struct{}{}
opts.CacheTTLDays = data.CacheTTLDays.ValueInt64()
}
if !data.DevcontainerDir.IsNull() {
env["ENVBUILDER_DEVCONTAINER_DIR"] = tfValueToString(data.DevcontainerDir)
overrides["ENVBUILDER_DEVCONTAINER_DIR"] = struct{}{}
opts.DevcontainerDir = data.DevcontainerDir.ValueString()
}
if !data.DevcontainerJSONPath.IsNull() {
env["ENVBUILDER_DEVCONTAINER_JSON_PATH"] = tfValueToString(data.DevcontainerJSONPath)
overrides["ENVBUILDER_DEVCONTAINER_JSON_PATH"] = struct{}{}
opts.DevcontainerJSONPath = data.DevcontainerJSONPath.ValueString()
}
if !data.DockerfilePath.IsNull() {
env["ENVBUILDER_DOCKERFILE_PATH"] = tfValueToString(data.DockerfilePath)
overrides["ENVBUILDER_DOCKERFILE_PATH"] = struct{}{}
opts.DockerfilePath = data.DockerfilePath.ValueString()
}
if !data.DockerConfigBase64.IsNull() {
env["ENVBUILDER_DOCKER_CONFIG_BASE64"] = tfValueToString(data.DockerConfigBase64)
overrides["ENVBUILDER_DOCKER_CONFIG_BASE64"] = struct{}{}
opts.DockerConfigBase64 = data.DockerConfigBase64.ValueString()
}
if !data.ExitOnBuildFailure.IsNull() {
env["ENVBUILDER_EXIT_ON_BUILD_FAILURE"] = tfValueToString(data.ExitOnBuildFailure)
overrides["ENVBUILDER_EXIT_ON_BUILD_FAILURE"] = struct{}{}
opts.ExitOnBuildFailure = data.ExitOnBuildFailure.ValueBool()
}
if !data.FallbackImage.IsNull() {
env["ENVBUILDER_FALLBACK_IMAGE"] = tfValueToString(data.FallbackImage)
overrides["ENVBUILDER_FALLBACK_IMAGE"] = struct{}{}
opts.FallbackImage = data.FallbackImage.ValueString()
}
if !data.GitCloneDepth.IsNull() {
env["ENVBUILDER_GIT_CLONE_DEPTH"] = tfValueToString(data.GitCloneDepth)
overrides["ENVBUILDER_GIT_CLONE_DEPTH"] = struct{}{}
opts.GitCloneDepth = data.GitCloneDepth.ValueInt64()
}
if !data.GitCloneSingleBranch.IsNull() {
env["ENVBUILDER_GIT_CLONE_SINGLE_BRANCH"] = tfValueToString(data.GitCloneSingleBranch)
overrides["ENVBUILDER_GIT_CLONE_SINGLE_BRANCH"] = struct{}{}
opts.GitCloneSingleBranch = data.GitCloneSingleBranch.ValueBool()
}
if !data.GitHTTPProxyURL.IsNull() {
env["ENVBUILDER_GIT_HTTP_PROXY_URL"] = tfValueToString(data.GitHTTPProxyURL)
overrides["ENVBUILDER_GIT_HTTP_PROXY_URL"] = struct{}{}
opts.GitHTTPProxyURL = data.GitHTTPProxyURL.ValueString()
}
if !data.GitSSHPrivateKeyPath.IsNull() {
env["ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH"] = tfValueToString(data.GitSSHPrivateKeyPath)
overrides["ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH"] = struct{}{}
opts.GitSSHPrivateKeyPath = data.GitSSHPrivateKeyPath.ValueString()
}
if !data.GitUsername.IsNull() {
env["ENVBUILDER_GIT_USERNAME"] = tfValueToString(data.GitUsername)
overrides["ENVBUILDER_GIT_USERNAME"] = struct{}{}
opts.GitUsername = data.GitUsername.ValueString()
}
if !data.GitPassword.IsNull() {
env["ENVBUILDER_GIT_PASSWORD"] = tfValueToString(data.GitPassword)
overrides["ENVBUILDER_GIT_PASSWORD"] = struct{}{}
opts.GitPassword = data.GitPassword.ValueString()
}
if !data.IgnorePaths.IsNull() {
env["ENVBUILDER_IGNORE_PATHS"] = strings.Join(tfListToStringSlice(data.IgnorePaths), ",")
overrides["ENVBUILDER_IGNORE_PATHS"] = struct{}{}
opts.IgnorePaths = tfListToStringSlice(data.IgnorePaths)
}
if !data.Insecure.IsNull() {
env["ENVBUILDER_INSECURE"] = tfValueToString(data.Insecure)
overrides["ENVBUILDER_INSECURE"] = struct{}{}
opts.Insecure = data.Insecure.ValueBool()
}
// Default to remote build mode.
if data.RemoteRepoBuildMode.IsNull() {
env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = "true"
opts.RemoteRepoBuildMode = true
} else {
env["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = tfValueToString(data.RemoteRepoBuildMode)
overrides["ENVBUILDER_REMOTE_REPO_BUILD_MODE"] = struct{}{}
opts.RemoteRepoBuildMode = data.RemoteRepoBuildMode.ValueBool()
}
if !data.SSLCertBase64.IsNull() {
env["ENVBUILDER_SSL_CERT_BASE64"] = tfValueToString(data.SSLCertBase64)
overrides["ENVBUILDER_SSL_CERT_BASE64"] = struct{}{}
opts.SSLCertBase64 = data.SSLCertBase64.ValueString()
}
if !data.Verbose.IsNull() {
env["ENVBUILDER_VERBOSE"] = tfValueToString(data.Verbose)
overrides["ENVBUILDER_VERBOSE"] = struct{}{}
opts.Verbose = data.Verbose.ValueBool()
}
if !data.WorkspaceFolder.IsNull() {
env["ENVBUILDER_WORKSPACE_FOLDER"] = tfValueToString(data.WorkspaceFolder)
overrides["ENVBUILDER_WORKSPACE_FOLDER"] = struct{}{}
opts.WorkspaceFolder = data.WorkspaceFolder.ValueString()
}
// Do ExtraEnv last so that it can override any other values.
// With one exception: ENVBUILDER_CACHE_REPO and ENVBUILDER_GIT_URL are required and should not be overridden.
// Other values set by the provider may be overridden, but will generate a warning.
var diag, ds diag.Diagnostics
if !data.ExtraEnv.IsNull() {
for key, elem := range data.ExtraEnv.Elements() {
switch key {
// These are required and should not be overridden.
case "ENVBUILDER_CACHE_REPO", "ENVBUILDER_GIT_URL":
diag.AddAttributeWarning(path.Root("extra_env"),
"Cannot override required environment variable",
fmt.Sprintf("The key %q in extra_env cannot be overridden.", key),
// convert extraEnv to a map for ease of use.
extraEnv := make(map[string]string)
for k, v := range data.ExtraEnv.Elements() {
extraEnv[k] = tfValueToString(v)
}
diags = append(diags, overrideOptionsFromExtraEnv(&opts, extraEnv, overrides)...)
return opts, diags
}
func overrideOptionsFromExtraEnv(opts *eboptions.Options, extraEnv map[string]string, overrides map[string]struct{}) diag.Diagnostics {
var diags diag.Diagnostics
// Make a map of the options for easy lookup.
optsMap := make(map[string]pflag.Value)
for _, opt := range opts.CLI() {
optsMap[opt.Env] = opt.Value
}
for key, val := range extraEnv {
switch key {
// These options may not be overridden.
case "ENVBUILDER_CACHE_REPO", "ENVBUILDER_GIT_URL":
diags.AddAttributeWarning(path.Root("extra_env"),
"Cannot override required environment variable",
fmt.Sprintf("The key %q in extra_env cannot be overridden.", key),
)
continue
default:
// Check if the option was set on the provider data model and generate a warning if so.
if _, overridden := overrides[key]; overridden {
diags.AddAttributeWarning(path.Root("extra_env"),
"Overriding provider environment variable",
fmt.Sprintf("The key %q in extra_env overrides an option set on the provider.", key),
)
}
// XXX: workaround for serpent behaviour where calling Set() on a
// string slice will append instead of replace: set to empty first.
if key == "ENVBUILDER_IGNORE_PATHS" {
_ = optsMap[key].Set("")
}
opt, found := optsMap[key]
if !found {
// ignore unknown keys
continue
}
if err := opt.Set(val); err != nil {
diags.AddAttributeError(path.Root("extra_env"),
"Invalid value for environment variable",
fmt.Sprintf("The key %q in extra_env has an invalid value: %s", key, err),
)
default:
if _, ok := env[key]; ok {
// This is a warning because it's possible that the user wants to override
// a value set by the provider.
diag.AddAttributeWarning(path.Root("extra_env"),
"Overriding provider environment variable",
fmt.Sprintf("The key %q in extra_env overrides an environment variable set by the provider.", key),
)
}
env[key] = tfValueToString(elem)
}
}
}
return diags
}
func computeEnvFromOptions(opts eboptions.Options, extraEnv map[string]string) map[string]string {
allEnvKeys := make(map[string]struct{})
for _, opt := range opts.CLI() {
if opt.Env == "" {
continue
}
allEnvKeys[opt.Env] = struct{}{}
}
// Only set the environment variables from opts that are not legacy options.
// Legacy options are those that are not prefixed with ENVBUILDER_.
// While we can detect when a legacy option is set, overriding it becomes
// problematic. Erring on the side of caution, we will not override legacy options.
isEnvbuilderOption := func(key string) bool {
switch key {
case "CODER_AGENT_URL", "CODER_AGENT_TOKEN", "CODER_AGENT_SUBSYSTEM":
return true // kinda
default:
return strings.HasPrefix(key, "ENVBUILDER_")
}
}
computed := make(map[string]string)
for _, opt := range opts.CLI() {
if opt.Env == "" {
continue
}
// TODO: remove this check once support for legacy options is removed.
if !isEnvbuilderOption(opt.Env) {
continue
}
var val string
if sa, ok := opt.Value.(*serpent.StringArray); ok {
val = strings.Join(sa.GetSlice(), ",")
} else {
val = opt.Value.String()
}
switch val {
case "", "false", "0":
// Skip zero values.
continue
}
computed[opt.Env] = val
}
// Merge in extraEnv, which may override values from opts.
// Skip any keys that are envbuilder options.
for key, val := range extraEnv {
if isEnvbuilderOption(key) {
continue
}
computed[key] = val
}
return computed
}
// setComputedEnv sets data.Env and data.EnvMap based on the values of the
// other fields in the model.
func (data *CachedImageResourceModel) setComputedEnv(ctx context.Context, env map[string]string) diag.Diagnostics {
var diag, ds diag.Diagnostics
data.EnvMap, ds = basetypes.NewMapValueFrom(ctx, types.StringType, env)
diag = append(diag, ds...)
data.Env, ds = basetypes.NewListValueFrom(ctx, types.StringType, sortedKeyValues(env))
@ -431,6 +551,16 @@ func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest
return
}
// Get the options from the data model.
opts, diags := optionsFromDataModel(data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Set the expected environment variables.
computedEnv := computeEnvFromOptions(opts, tfMapToStringMap(data.ExtraEnv))
resp.Diagnostics.Append(data.setComputedEnv(ctx, computedEnv)...)
// If the previous state is that Image == BuilderImage, then we previously did
// not find the image. We will need to run another cache probe.
if data.Image.Equal(data.BuilderImage) {
@ -478,9 +608,6 @@ func (r *CachedImageResource) Read(ctx context.Context, req resource.ReadRequest
data.Image = types.StringValue(fmt.Sprintf("%s@%s", data.CacheRepo.ValueString(), digest))
data.Exists = types.BoolValue(true)
// Set the expected environment variables.
resp.Diagnostics.Append(data.setComputedEnv(ctx)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@ -494,7 +621,18 @@ func (r *CachedImageResource) Create(ctx context.Context, req resource.CreateReq
return
}
cachedImg, err := r.runCacheProbe(ctx, data)
// Get the options from the data model.
opts, diags := optionsFromDataModel(data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Set the expected environment variables.
computedEnv := computeEnvFromOptions(opts, tfMapToStringMap(data.ExtraEnv))
resp.Diagnostics.Append(data.setComputedEnv(ctx, computedEnv)...)
cachedImg, err := runCacheProbe(ctx, data.BuilderImage.ValueString(), opts)
data.ID = types.StringValue(uuid.Nil.String())
data.Exists = types.BoolValue(err == nil)
if err != nil {
@ -517,9 +655,6 @@ func (r *CachedImageResource) Create(ctx context.Context, req resource.CreateReq
data.ID = types.StringValue(digest.String())
}
// Set the expected environment variables.
resp.Diagnostics.Append(data.setComputedEnv(ctx)...)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@ -553,7 +688,7 @@ func (r *CachedImageResource) Delete(ctx context.Context, req resource.DeleteReq
// runCacheProbe performs a 'fake build' of the requested image and ensures that
// all of the resulting layers of the image are present in the configured cache
// repo. Otherwise, returns an error.
func (r *CachedImageResource) runCacheProbe(ctx context.Context, data CachedImageResourceModel) (v1.Image, error) {
func runCacheProbe(ctx context.Context, builderImage string, opts eboptions.Options) (v1.Image, error) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "envbuilder-provider-cached-image-data-source")
if err != nil {
return nil, fmt.Errorf("unable to create temp directory: %s", err.Error())
@ -581,69 +716,53 @@ func (r *CachedImageResource) runCacheProbe(ctx context.Context, data CachedImag
// In order to correctly reproduce the final layer of the cached image, we
// need the envbuilder binary used to originally build the image!
envbuilderPath := filepath.Join(tmpDir, "envbuilder")
if err := extractEnvbuilderFromImage(ctx, data.BuilderImage.ValueString(), envbuilderPath); err != nil {
if err := extractEnvbuilderFromImage(ctx, builderImage, envbuilderPath); err != nil {
tflog.Error(ctx, "failed to fetch envbuilder binary from builder image", map[string]any{"err": err})
return nil, fmt.Errorf("failed to fetch the envbuilder binary from the builder image: %s", err.Error())
}
opts.BinaryPath = envbuilderPath
workspaceFolder := data.WorkspaceFolder.ValueString()
if workspaceFolder == "" {
workspaceFolder = filepath.Join(tmpDir, "workspace")
tflog.Debug(ctx, "workspace_folder not specified, using temp dir", map[string]any{"workspace_folder": workspaceFolder})
// We need a filesystem to work with.
opts.Filesystem = osfs.New("/")
// This should never be set to true, as this may be running outside of a container!
opts.ForceSafe = false
// We always want to get the cached image.
opts.GetCachedImage = true
// Log to the Terraform logger.
opts.Logger = tfLogFunc(ctx)
// We don't require users to set a workspace folder, but maybe there's a
// reason someone may need to.
if opts.WorkspaceFolder == "" {
opts.WorkspaceFolder = filepath.Join(tmpDir, "workspace")
if err := os.MkdirAll(opts.WorkspaceFolder, 0o755); err != nil {
return nil, fmt.Errorf("failed to create workspace folder: %w", err)
}
tflog.Debug(ctx, "workspace_folder not specified, using temp dir", map[string]any{"workspace_folder": opts.WorkspaceFolder})
}
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,
// Options related to compiling the devcontainer
BuildContextPath: data.BuildContextPath.ValueString(),
DevcontainerDir: data.DevcontainerDir.ValueString(),
DevcontainerJSONPath: data.DevcontainerJSONPath.ValueString(),
DockerfilePath: data.DockerfilePath.ValueString(),
DockerConfigBase64: data.DockerConfigBase64.ValueString(),
FallbackImage: data.FallbackImage.ValueString(),
// These options are required for cloning the Git repo
CacheTTLDays: data.CacheTTLDays.ValueInt64(),
GitURL: data.GitURL.ValueString(),
GitCloneDepth: data.GitCloneDepth.ValueInt64(),
GitCloneSingleBranch: data.GitCloneSingleBranch.ValueBool(),
GitUsername: data.GitUsername.ValueString(),
GitPassword: data.GitPassword.ValueString(),
GitSSHPrivateKeyPath: data.GitSSHPrivateKeyPath.ValueString(),
GitHTTPProxyURL: data.GitHTTPProxyURL.ValueString(),
RemoteRepoBuildMode: data.RemoteRepoBuildMode.ValueBool(),
RemoteRepoDir: filepath.Join(tmpDir, "repo"),
SSLCertBase64: data.SSLCertBase64.ValueString(),
// Other options
BaseImageCacheDir: data.BaseImageCacheDir.ValueString(),
BinaryPath: envbuilderPath, // needed to reproduce the final layer.
ExitOnBuildFailure: data.ExitOnBuildFailure.ValueBool(), // may wish to do this instead of fallback image?
Insecure: data.Insecure.ValueBool(), // might have internal CAs?
IgnorePaths: tfListToStringSlice(data.IgnorePaths), // may need to be specified?
// The below options are not relevant and are set to their zero value explicitly.
// They must be set by extra_env.
CoderAgentSubsystem: nil,
CoderAgentToken: "",
CoderAgentURL: "",
ExportEnvFile: "",
InitArgs: "",
InitCommand: "",
InitScript: "",
LayerCacheDir: "",
PostStartScriptPath: "",
PushImage: false, // This is only relevant when building.
SetupScript: "",
SkipRebuild: false,
// We need a place to clone the repo.
repoDir := filepath.Join(tmpDir, "repo")
if err := os.MkdirAll(repoDir, 0o755); err != nil {
return nil, fmt.Errorf("failed to create repo dir: %w", err)
}
opts.RemoteRepoDir = repoDir
// The below options are not relevant and are set to their zero value
// explicitly.
// They must be set by extra_env to be used in the final builder image.
opts.CoderAgentSubsystem = nil
opts.CoderAgentToken = ""
opts.CoderAgentURL = ""
opts.ExportEnvFile = ""
opts.InitArgs = ""
opts.InitCommand = ""
opts.InitScript = ""
opts.LayerCacheDir = ""
opts.PostStartScriptPath = ""
opts.PushImage = false
opts.SetupScript = ""
opts.SkipRebuild = false
return envbuilder.RunCacheProbe(ctx, opts)
}
@ -764,6 +883,16 @@ func tfListToStringSlice(l types.List) []string {
return ss
}
// tfMapToStringMap converts a types.Map to a map[string]string by calling
// tfValueToString on each element.
func tfMapToStringMap(m types.Map) map[string]string {
res := make(map[string]string)
for k, v := range m.Elements() {
res[k] = tfValueToString(v)
}
return res
}
// tfLogFunc is an adapter to envbuilder/log.Func.
func tfLogFunc(ctx context.Context) eblog.Func {
return func(level eblog.Level, format string, args ...any) {

View file

@ -20,8 +20,10 @@ func TestAccCachedImageResource(t *testing.T) {
defer cancel()
for _, tc := range []struct {
name string
files map[string]string
name string
files map[string]string
extraEnv map[string]string
assertEnv func(t *testing.T, deps testDependencies) resource.TestCheckFunc
}{
{
// This test case is the simplest possible case: a devcontainer.json.
@ -31,6 +33,23 @@ func TestAccCachedImageResource(t *testing.T) {
files: map[string]string{
".devcontainer/devcontainer.json": `{"image": "localhost:5000/test-ubuntu:latest"}`,
},
extraEnv: map[string]string{
"FOO": testEnvValue,
"ENVBUILDER_GIT_URL": "https://not.the.real.git/url",
"ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo",
},
assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc {
return resource.ComposeAggregateTestCheckFunc(
assertEnv(t,
"ENVBUILDER_CACHE_REPO", deps.CacheRepo,
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
"ENVBUILDER_GIT_URL", deps.Repo.URL,
"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
"ENVBUILDER_VERBOSE", "true",
"FOO", "bar\nbaz",
),
)
},
},
{
// This test case includes a Dockerfile in addition to the devcontainer.json.
@ -42,14 +61,61 @@ func TestAccCachedImageResource(t *testing.T) {
".devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
RUN date > /date.txt`,
},
extraEnv: map[string]string{
"FOO": testEnvValue,
"ENVBUILDER_GIT_URL": "https://not.the.real.git/url",
"ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo",
},
assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc {
return resource.ComposeAggregateTestCheckFunc(
assertEnv(t,
"ENVBUILDER_CACHE_REPO", deps.CacheRepo,
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
"ENVBUILDER_GIT_URL", deps.Repo.URL,
"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
"ENVBUILDER_VERBOSE", "true",
"FOO", "bar\nbaz",
),
)
},
},
{
// This test case ensures that parameters passed via extra_env are
// handled correctly.
name: "extra_env",
files: map[string]string{
"path/to/.devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`,
"path/to/.devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
RUN date > /date.txt`,
},
extraEnv: map[string]string{
"FOO": testEnvValue,
"ENVBUILDER_GIT_URL": "https://not.the.real.git/url",
"ENVBUILDER_CACHE_REPO": "not-the-real-cache-repo",
"ENVBUILDER_DEVCONTAINER_DIR": "path/to/.devcontainer",
"ENVBUILDER_DEVCONTAINER_JSON_PATH": "path/to/.devcontainer/devcontainer.json",
"ENVBUILDER_DOCKERFILE_PATH": "path/to/.devcontainer/Dockerfile",
},
assertEnv: func(t *testing.T, deps testDependencies) resource.TestCheckFunc {
return resource.ComposeAggregateTestCheckFunc(
assertEnv(t,
"ENVBUILDER_CACHE_REPO", deps.CacheRepo,
"ENVBUILDER_DEVCONTAINER_DIR", "path/to/.devcontainer",
"ENVBUILDER_DEVCONTAINER_JSON_PATH", "path/to/.devcontainer/devcontainer.json",
"ENVBUILDER_DOCKERFILE_PATH", "path/to/.devcontainer/Dockerfile",
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
"ENVBUILDER_GIT_URL", deps.Repo.URL,
"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
"ENVBUILDER_VERBOSE", "true",
"FOO", "bar\nbaz",
),
)
},
},
} {
t.Run(tc.name, func(t *testing.T) {
//nolint: paralleltest
deps := setup(ctx, t, tc.files)
deps.ExtraEnv["FOO"] = testEnvValue
deps.ExtraEnv["ENVBUILDER_GIT_URL"] = "https://not.the.real.git/url"
deps.ExtraEnv["ENVBUILDER_CACHE_REPO"] = "not-the-real-cache-repo"
deps := setup(ctx, t, tc.extraEnv, tc.files)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
@ -71,14 +137,13 @@ RUN date > /date.txt`,
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage),
// Inputs should still be present.
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL),
// Should be empty
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"),
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"),
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"),
// Environment variables
assertEnv(t, deps),
tc.assertEnv(t, deps),
),
ExpectNonEmptyPlan: true, // TODO: check the plan.
},
@ -93,14 +158,13 @@ RUN date > /date.txt`,
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "image", deps.BuilderImage),
// Inputs should still be present.
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL),
// Should be empty
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"),
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_password"),
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "cache_ttl_days"),
// Environment variables
assertEnv(t, deps),
tc.assertEnv(t, deps),
),
ExpectNonEmptyPlan: true, // TODO: check the plan.
},
@ -113,7 +177,6 @@ RUN date > /date.txt`,
Check: resource.ComposeAggregateTestCheckFunc(
// Inputs should still be present.
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "cache_repo", deps.CacheRepo),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "extra_env.FOO", "bar\nbaz"),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "git_url", deps.Repo.URL),
// Should be empty
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "git_username"),
@ -125,7 +188,7 @@ RUN date > /date.txt`,
resource.TestCheckResourceAttrSet("envbuilder_cached_image.test", "image"),
resource.TestCheckResourceAttrWith("envbuilder_cached_image.test", "image", quotedPrefix(deps.CacheRepo)),
// Environment variables
assertEnv(t, deps),
tc.assertEnv(t, deps),
),
},
// 5) Should produce an empty plan after apply
@ -144,28 +207,31 @@ RUN date > /date.txt`,
}
}
// assertEnv is a test helper that checks the environment variables set on the
// cached image resource based on the provided test dependencies.
func assertEnv(t *testing.T, deps testDependencies) resource.TestCheckFunc {
// assertEnv is a test helper that checks the environment variables, in order,
// on both the env and env_map attributes of the cached image resource.
func assertEnv(t *testing.T, kvs ...string) resource.TestCheckFunc {
t.Helper()
return resource.ComposeAggregateTestCheckFunc(
// Check that the environment variables are set correctly.
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.0", fmt.Sprintf("ENVBUILDER_CACHE_REPO=%s", deps.CacheRepo)),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.1", fmt.Sprintf("ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH=%s", deps.Repo.Key)),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.2", fmt.Sprintf("ENVBUILDER_GIT_URL=%s", deps.Repo.URL)),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.3", "ENVBUILDER_REMOTE_REPO_BUILD_MODE=true"),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.4", "ENVBUILDER_VERBOSE=true"),
// Check that the extra environment variables are set correctly.
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env.5", "FOO=bar\nbaz"),
// We should not have any other environment variables set.
resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", "env.6"),
if len(kvs)%2 != 0 {
t.Fatalf("assertEnv: expected an even number of key-value pairs, got %d", len(kvs))
}
// Check that the same values are set in env_map.
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_CACHE_REPO", deps.CacheRepo),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_GIT_URL", deps.Repo.URL),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true"),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.ENVBUILDER_VERBOSE", "true"),
resource.TestCheckResourceAttr("envbuilder_cached_image.test", "env_map.FOO", "bar\nbaz"),
)
funcs := make([]resource.TestCheckFunc, 0)
for i := 0; i < len(kvs); i += 2 {
resKey := fmt.Sprintf("env.%d", len(funcs))
resVal := fmt.Sprintf("%s=%s", kvs[i], kvs[i+1])
fn := resource.TestCheckResourceAttr("envbuilder_cached_image.test", resKey, resVal)
funcs = append(funcs, fn)
}
lastKey := fmt.Sprintf("env.%d", len(funcs))
lastFn := resource.TestCheckNoResourceAttr("envbuilder_cached_image.test", lastKey)
funcs = append(funcs, lastFn)
for i := 0; i < len(kvs); i += 2 {
resKey := fmt.Sprintf("env_map.%s", kvs[i])
fn := resource.TestCheckResourceAttr("envbuilder_cached_image.test", resKey, kvs[i+1])
funcs = append(funcs, fn)
}
return resource.ComposeAggregateTestCheckFunc(funcs...)
}

View file

@ -0,0 +1,359 @@
package provider
import (
"testing"
eboptions "github.com/coder/envbuilder/options"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stretchr/testify/assert"
)
func Test_optionsFromDataModel(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
data CachedImageResourceModel
expectOpts eboptions.Options
expectNumErrorDiags int
expectNumWarningDiags int
}{
{
name: "required only",
data: CachedImageResourceModel{
BuilderImage: basetypes.NewStringValue("envbuilder:latest"),
CacheRepo: basetypes.NewStringValue("localhost:5000/cache"),
GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"),
},
expectOpts: eboptions.Options{
CacheRepo: "localhost:5000/cache",
GitURL: "git@git.local/devcontainer.git",
RemoteRepoBuildMode: true,
},
},
{
name: "all options without extra_env",
data: CachedImageResourceModel{
BuilderImage: basetypes.NewStringValue("envbuilder:latest"),
CacheRepo: basetypes.NewStringValue("localhost:5000/cache"),
GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"),
BaseImageCacheDir: basetypes.NewStringValue("/tmp/cache"),
BuildContextPath: basetypes.NewStringValue("."),
CacheTTLDays: basetypes.NewInt64Value(7),
DevcontainerDir: basetypes.NewStringValue(".devcontainer"),
DevcontainerJSONPath: basetypes.NewStringValue(".devcontainer/devcontainer.json"),
DockerfilePath: basetypes.NewStringValue("Dockerfile"),
DockerConfigBase64: basetypes.NewStringValue("some base64"),
ExitOnBuildFailure: basetypes.NewBoolValue(true),
// ExtraEnv: map[string]basetypes.Value{},
FallbackImage: basetypes.NewStringValue("fallback"),
GitCloneDepth: basetypes.NewInt64Value(1),
GitCloneSingleBranch: basetypes.NewBoolValue(true),
GitHTTPProxyURL: basetypes.NewStringValue("http://proxy"),
GitPassword: basetypes.NewStringValue("password"),
GitSSHPrivateKeyPath: basetypes.NewStringValue("/tmp/id_rsa"),
GitUsername: basetypes.NewStringValue("user"),
IgnorePaths: listValue("ignore", "paths"),
Insecure: basetypes.NewBoolValue(true),
RemoteRepoBuildMode: basetypes.NewBoolValue(false),
SSLCertBase64: basetypes.NewStringValue("cert"),
Verbose: basetypes.NewBoolValue(true),
WorkspaceFolder: basetypes.NewStringValue("workspace"),
},
expectOpts: eboptions.Options{
CacheRepo: "localhost:5000/cache",
GitURL: "git@git.local/devcontainer.git",
BaseImageCacheDir: "/tmp/cache",
BuildContextPath: ".",
CacheTTLDays: 7,
DevcontainerDir: ".devcontainer",
DevcontainerJSONPath: ".devcontainer/devcontainer.json",
DockerfilePath: "Dockerfile",
DockerConfigBase64: "some base64",
ExitOnBuildFailure: true,
FallbackImage: "fallback",
GitCloneDepth: 1,
GitCloneSingleBranch: true,
GitHTTPProxyURL: "http://proxy",
GitPassword: "password",
GitSSHPrivateKeyPath: "/tmp/id_rsa",
GitUsername: "user",
IgnorePaths: []string{"ignore", "paths"},
Insecure: true,
RemoteRepoBuildMode: false,
SSLCertBase64: "cert",
Verbose: true,
WorkspaceFolder: "workspace",
},
},
{
name: "extra env override",
data: CachedImageResourceModel{
BuilderImage: basetypes.NewStringValue("envbuilder:latest"),
CacheRepo: basetypes.NewStringValue("localhost:5000/cache"),
GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"),
ExtraEnv: extraEnvMap(t,
"CODER_AGENT_TOKEN", "token",
"CODER_AGENT_URL", "http://coder",
"FOO", "bar",
),
},
expectOpts: eboptions.Options{
CacheRepo: "localhost:5000/cache",
GitURL: "git@git.local/devcontainer.git",
RemoteRepoBuildMode: true,
CoderAgentToken: "token",
CoderAgentURL: "http://coder",
},
},
{
name: "extra_env override warnings",
data: CachedImageResourceModel{
BuilderImage: basetypes.NewStringValue("envbuilder:latest"),
CacheRepo: basetypes.NewStringValue("localhost:5000/cache"),
GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"),
BaseImageCacheDir: basetypes.NewStringValue("/tmp/cache"),
BuildContextPath: basetypes.NewStringValue("."),
CacheTTLDays: basetypes.NewInt64Value(7),
DevcontainerDir: basetypes.NewStringValue(".devcontainer"),
DevcontainerJSONPath: basetypes.NewStringValue(".devcontainer/devcontainer.json"),
DockerfilePath: basetypes.NewStringValue("Dockerfile"),
DockerConfigBase64: basetypes.NewStringValue("some base64"),
ExitOnBuildFailure: basetypes.NewBoolValue(true),
// ExtraEnv: map[string]basetypes.Value{},
FallbackImage: basetypes.NewStringValue("fallback"),
GitCloneDepth: basetypes.NewInt64Value(1),
GitCloneSingleBranch: basetypes.NewBoolValue(true),
GitHTTPProxyURL: basetypes.NewStringValue("http://proxy"),
GitPassword: basetypes.NewStringValue("password"),
GitSSHPrivateKeyPath: basetypes.NewStringValue("/tmp/id_rsa"),
GitUsername: basetypes.NewStringValue("user"),
IgnorePaths: listValue("ignore", "paths"),
Insecure: basetypes.NewBoolValue(true),
RemoteRepoBuildMode: basetypes.NewBoolValue(false),
SSLCertBase64: basetypes.NewStringValue("cert"),
Verbose: basetypes.NewBoolValue(true),
WorkspaceFolder: basetypes.NewStringValue("workspace"),
ExtraEnv: extraEnvMap(t,
"ENVBUILDER_CACHE_REPO", "override",
"ENVBUILDER_GIT_URL", "override",
"ENVBUILDER_BASE_IMAGE_CACHE_DIR", "override",
"ENVBUILDER_BUILD_CONTEXT_PATH", "override",
"ENVBUILDER_CACHE_TTL_DAYS", "8",
"ENVBUILDER_DEVCONTAINER_DIR", "override",
"ENVBUILDER_DEVCONTAINER_JSON_PATH", "override",
"ENVBUILDER_DOCKERFILE_PATH", "override",
"ENVBUILDER_DOCKER_CONFIG_BASE64", "override",
"ENVBUILDER_EXIT_ON_BUILD_FAILURE", "false",
"ENVBUILDER_FALLBACK_IMAGE", "override",
"ENVBUILDER_GIT_CLONE_DEPTH", "2",
"ENVBUILDER_GIT_CLONE_SINGLE_BRANCH", "false",
"ENVBUILDER_GIT_HTTP_PROXY_URL", "override",
"ENVBUILDER_GIT_PASSWORD", "override",
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", "override",
"ENVBUILDER_GIT_USERNAME", "override",
"ENVBUILDER_IGNORE_PATHS", "override",
"ENVBUILDER_INSECURE", "false",
"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
"ENVBUILDER_SSL_CERT_BASE64", "override",
"ENVBUILDER_VERBOSE", "false",
"ENVBUILDER_WORKSPACE_FOLDER", "override",
"FOO", "bar",
),
},
expectOpts: eboptions.Options{
// not overridden
CacheRepo: "localhost:5000/cache",
GitURL: "git@git.local/devcontainer.git",
// overridden
BaseImageCacheDir: "override",
BuildContextPath: "override",
CacheTTLDays: 8,
DevcontainerDir: "override",
DevcontainerJSONPath: "override",
DockerfilePath: "override",
DockerConfigBase64: "override",
ExitOnBuildFailure: false,
FallbackImage: "override",
GitCloneDepth: 2,
GitCloneSingleBranch: false,
GitHTTPProxyURL: "override",
GitPassword: "override",
GitSSHPrivateKeyPath: "override",
GitUsername: "override",
IgnorePaths: []string{"override"},
Insecure: false,
RemoteRepoBuildMode: true,
SSLCertBase64: "override",
Verbose: false,
WorkspaceFolder: "override",
},
expectNumWarningDiags: 23,
},
{
name: "extra_env override errors",
data: CachedImageResourceModel{
BuilderImage: basetypes.NewStringValue("envbuilder:latest"),
CacheRepo: basetypes.NewStringValue("localhost:5000/cache"),
GitURL: basetypes.NewStringValue("git@git.local/devcontainer.git"),
ExtraEnv: extraEnvMap(t,
"ENVBUILDER_CACHE_TTL_DAYS", "not a number",
"ENVBUILDER_VERBOSE", "not a bool",
"FOO", "bar",
),
},
expectOpts: eboptions.Options{
// not overridden
CacheRepo: "localhost:5000/cache",
GitURL: "git@git.local/devcontainer.git",
RemoteRepoBuildMode: true,
},
expectNumErrorDiags: 2,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actual, diags := optionsFromDataModel(tc.data)
assert.Equal(t, tc.expectNumErrorDiags, diags.ErrorsCount())
assert.Equal(t, tc.expectNumWarningDiags, diags.WarningsCount())
assert.EqualValues(t, tc.expectOpts, actual)
})
}
}
func Test_computeEnvFromOptions(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
opts eboptions.Options
extraEnv map[string]string
expectEnv map[string]string
}{
{
name: "empty",
opts: eboptions.Options{},
expectEnv: map[string]string{},
},
{
name: "all options",
opts: eboptions.Options{
BaseImageCacheDir: "string",
BinaryPath: "string",
BuildContextPath: "string",
CacheRepo: "string",
CacheTTLDays: 1,
CoderAgentSubsystem: []string{"one", "two"},
CoderAgentToken: "string",
CoderAgentURL: "string",
DevcontainerDir: "string",
DevcontainerJSONPath: "string",
DockerConfigBase64: "string",
DockerfilePath: "string",
ExitOnBuildFailure: true,
ExportEnvFile: "string",
FallbackImage: "string",
ForceSafe: true,
GetCachedImage: true,
GitCloneDepth: 1,
GitCloneSingleBranch: true,
GitHTTPProxyURL: "string",
GitPassword: "string",
GitSSHPrivateKeyPath: "string",
GitURL: "string",
GitUsername: "string",
IgnorePaths: []string{"one", "two"},
InitArgs: "string",
InitCommand: "string",
InitScript: "string",
Insecure: true,
LayerCacheDir: "string",
PostStartScriptPath: "string",
PushImage: true,
RemoteRepoBuildMode: true,
RemoteRepoDir: "string",
SetupScript: "string",
SkipRebuild: true,
SSLCertBase64: "string",
Verbose: true,
WorkspaceFolder: "string",
},
extraEnv: map[string]string{
"ENVBUILDER_SOMETHING": "string", // should be ignored
"FOO": "bar", // should be included
},
expectEnv: map[string]string{
"CODER_AGENT_SUBSYSTEM": "one,two",
"CODER_AGENT_TOKEN": "string",
"CODER_AGENT_URL": "string",
"ENVBUILDER_BASE_IMAGE_CACHE_DIR": "string",
"ENVBUILDER_BINARY_PATH": "string",
"ENVBUILDER_BUILD_CONTEXT_PATH": "string",
"ENVBUILDER_CACHE_REPO": "string",
"ENVBUILDER_CACHE_TTL_DAYS": "1",
"ENVBUILDER_DEVCONTAINER_DIR": "string",
"ENVBUILDER_DEVCONTAINER_JSON_PATH": "string",
"ENVBUILDER_DOCKER_CONFIG_BASE64": "string",
"ENVBUILDER_DOCKERFILE_PATH": "string",
"ENVBUILDER_EXIT_ON_BUILD_FAILURE": "true",
"ENVBUILDER_EXPORT_ENV_FILE": "string",
"ENVBUILDER_FALLBACK_IMAGE": "string",
"ENVBUILDER_FORCE_SAFE": "true",
"ENVBUILDER_GET_CACHED_IMAGE": "true",
"ENVBUILDER_GIT_CLONE_DEPTH": "1",
"ENVBUILDER_GIT_CLONE_SINGLE_BRANCH": "true",
"ENVBUILDER_GIT_HTTP_PROXY_URL": "string",
"ENVBUILDER_GIT_PASSWORD": "string",
"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": "string",
"ENVBUILDER_GIT_URL": "string",
"ENVBUILDER_GIT_USERNAME": "string",
"ENVBUILDER_IGNORE_PATHS": "one,two",
"ENVBUILDER_INIT_ARGS": "string",
"ENVBUILDER_INIT_COMMAND": "string",
"ENVBUILDER_INIT_SCRIPT": "string",
"ENVBUILDER_INSECURE": "true",
"ENVBUILDER_LAYER_CACHE_DIR": "string",
"ENVBUILDER_POST_START_SCRIPT_PATH": "string",
"ENVBUILDER_PUSH_IMAGE": "true",
"ENVBUILDER_REMOTE_REPO_BUILD_MODE": "true",
"ENVBUILDER_REMOTE_REPO_DIR": "string",
"ENVBUILDER_SETUP_SCRIPT": "string",
"ENVBUILDER_SKIP_REBUILD": "true",
"ENVBUILDER_SSL_CERT_BASE64": "string",
"ENVBUILDER_VERBOSE": "true",
"ENVBUILDER_WORKSPACE_FOLDER": "string",
"FOO": "bar",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.extraEnv == nil {
tc.extraEnv = map[string]string{}
}
actual := computeEnvFromOptions(tc.opts, tc.extraEnv)
assert.EqualValues(t, tc.expectEnv, actual)
})
}
}
func listValue(vs ...string) basetypes.ListValue {
vals := make([]attr.Value, len(vs))
for i, s := range vs {
vals[i] = basetypes.NewStringValue(s)
}
return basetypes.NewListValueMust(basetypes.StringType{}, vals)
}
func extraEnvMap(t *testing.T, kvs ...string) basetypes.MapValue {
t.Helper()
if len(kvs)%2 != 0 {
t.Fatalf("extraEnvMap: expected even number of key-value pairs, got %d", len(kvs))
}
vals := make(map[string]attr.Value)
for i := 0; i < len(kvs); i += 2 {
vals[kvs[i]] = basetypes.NewStringValue(kvs[i+1])
}
return basetypes.NewMapValueMust(basetypes.StringType{}, vals)
}

View file

@ -49,14 +49,14 @@ func (d *testDependencies) Config(t testing.TB) string {
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 }}
}
git_url = {{ quote .Repo.URL }}
git_ssh_private_key_path = {{ quote .Repo.Key }}
verbose = true
}`
fm := template.FuncMap{"quote": quote}
@ -71,7 +71,7 @@ func quote(s string) string {
return fmt.Sprintf("%q", s)
}
func setup(ctx context.Context, t testing.TB, files map[string]string) testDependencies {
func setup(ctx context.Context, t testing.TB, extraEnv, files map[string]string) testDependencies {
t.Helper()
envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "localhost:5000/envbuilder")
@ -89,7 +89,7 @@ func setup(ctx context.Context, t testing.TB, files map[string]string) testDepen
return testDependencies{
BuilderImage: envbuilderImageRef,
CacheRepo: reg + "/test",
ExtraEnv: make(map[string]string),
ExtraEnv: extraEnv,
Repo: gitRepo,
}
}
@ -106,18 +106,38 @@ func seedCache(ctx context.Context, t testing.TB, deps testDependencies) {
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: []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",
},
Env: seedDockerEnv,
Labels: map[string]string{
testContainerLabel: "true",
},