mirror of
https://github.com/coder/terraform-provider-envbuilder.git
synced 2025-07-25 21:17:51 +00:00
fix(internal/provider): correctly override from extra_env (#44)
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.
This commit is contained in:
parent
e35030b39f
commit
23f2cf5f48
4 changed files with 737 additions and 163 deletions
|
@ -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) {
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
|
|
359
internal/provider/provider_internal_test.go
Normal file
359
internal/provider/provider_internal_test.go
Normal 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)
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue