mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
1e07ec49a6
## Description Implements the server-side merge logic for the `merge_strategy` attribute added to `coder_env` in [terraform-provider-coder v2.15.0](https://github.com/coder/terraform-provider-coder/pull/489). This allows template authors to control how duplicate environment variable names are combined across multiple `coder_env` resources. Relates to https://github.com/coder/coder/issues/21885 ## Supported strategies | Strategy | Behavior | |----------|----------| | `replace` (default) | Last value wins — backward compatible | | `append` | Joins values with `:` separator (e.g. PATH additions) | | `prepend` | Prepends value with `:` separator | | `error` | Fails the build if the variable is already defined | ## Example ```hcl resource "coder_env" "path_tools" { agent_id = coder_agent.dev.id name = "PATH" value = "/home/coder/tools/bin" merge_strategy = "append" } ``` ## Changes - **Proto**: Added `merge_strategy` field to `Env` message in `provisioner.proto` - **State reader**: Updated `agentEnvAttributes` struct and proto construction in `resources.go` - **Merge logic**: Added `mergeExtraEnvs()` function in `provisionerdserver.go` with strategy-aware merging for both agent envs and devcontainer subagent envs - **Tests**: 15 unit tests covering all strategies, edge cases (empty values, mixed strategies, multiple appends) - **Dependency**: Bumped `terraform-provider-coder` v2.14.0 → v2.15.0 - **Fixtures**: Updated `duplicate-env-keys` test fixtures and golden files ## Ordering When multiple resources `append` or `prepend` to the same key, they are processed in alphabetical order by Terraform resource address (per the determinism fix in #22706).
167 lines
4.3 KiB
Go
167 lines
4.3 KiB
Go
package provisionerdserver_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
|
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
|
|
)
|
|
|
|
func TestMergeExtraEnvs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initial map[string]string
|
|
envs []*sdkproto.Env
|
|
expected map[string]string
|
|
expectErr string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
initial: map[string]string{},
|
|
envs: nil,
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "default_replace",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "FOO", Value: "bar"},
|
|
},
|
|
expected: map[string]string{"FOO": "bar"},
|
|
},
|
|
{
|
|
name: "explicit_replace",
|
|
initial: map[string]string{"FOO": "old"},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "FOO", Value: "new", MergeStrategy: "replace"},
|
|
},
|
|
expected: map[string]string{"FOO": "new"},
|
|
},
|
|
{
|
|
name: "empty_strategy_defaults_to_replace",
|
|
initial: map[string]string{"FOO": "old"},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "FOO", Value: "new", MergeStrategy: ""},
|
|
},
|
|
expected: map[string]string{"FOO": "new"},
|
|
},
|
|
{
|
|
name: "append_to_existing",
|
|
initial: map[string]string{"PATH": "/usr/bin"},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/custom/bin", MergeStrategy: "append"},
|
|
},
|
|
expected: map[string]string{"PATH": "/usr/bin:/custom/bin"},
|
|
},
|
|
{
|
|
name: "append_no_existing",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/custom/bin", MergeStrategy: "append"},
|
|
},
|
|
expected: map[string]string{"PATH": "/custom/bin"},
|
|
},
|
|
{
|
|
name: "append_to_empty_value",
|
|
initial: map[string]string{"PATH": ""},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/custom/bin", MergeStrategy: "append"},
|
|
},
|
|
expected: map[string]string{"PATH": "/custom/bin"},
|
|
},
|
|
{
|
|
name: "prepend_to_existing",
|
|
initial: map[string]string{"PATH": "/usr/bin"},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/custom/bin", MergeStrategy: "prepend"},
|
|
},
|
|
expected: map[string]string{"PATH": "/custom/bin:/usr/bin"},
|
|
},
|
|
{
|
|
name: "prepend_no_existing",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/custom/bin", MergeStrategy: "prepend"},
|
|
},
|
|
expected: map[string]string{"PATH": "/custom/bin"},
|
|
},
|
|
{
|
|
name: "error_no_duplicate",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "FOO", Value: "bar", MergeStrategy: "error"},
|
|
},
|
|
expected: map[string]string{"FOO": "bar"},
|
|
},
|
|
{
|
|
name: "error_with_duplicate",
|
|
initial: map[string]string{"FOO": "existing"},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "FOO", Value: "new", MergeStrategy: "error"},
|
|
},
|
|
expectErr: "duplicate env var",
|
|
},
|
|
{
|
|
name: "multiple_appends_same_key",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/a/bin", MergeStrategy: "append"},
|
|
{Name: "PATH", Value: "/b/bin", MergeStrategy: "append"},
|
|
},
|
|
expected: map[string]string{"PATH": "/a/bin:/b/bin"},
|
|
},
|
|
{
|
|
name: "multiple_prepends_same_key",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/a/bin", MergeStrategy: "prepend"},
|
|
{Name: "PATH", Value: "/b/bin", MergeStrategy: "prepend"},
|
|
},
|
|
expected: map[string]string{"PATH": "/b/bin:/a/bin"},
|
|
},
|
|
{
|
|
name: "mixed_strategies",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/first", MergeStrategy: "append"},
|
|
{Name: "PATH", Value: "/override", MergeStrategy: "replace"},
|
|
},
|
|
expected: map[string]string{"PATH": "/override"},
|
|
},
|
|
{
|
|
name: "mixed_keys",
|
|
initial: map[string]string{},
|
|
envs: []*sdkproto.Env{
|
|
{Name: "PATH", Value: "/a", MergeStrategy: "append"},
|
|
{Name: "HOME", Value: "/home/user"},
|
|
{Name: "PATH", Value: "/b", MergeStrategy: "append"},
|
|
},
|
|
expected: map[string]string{
|
|
"PATH": "/a:/b",
|
|
"HOME": "/home/user",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
env := make(map[string]string)
|
|
for k, v := range tc.initial {
|
|
env[k] = v
|
|
}
|
|
err := provisionerdserver.MergeExtraEnvs(env, tc.envs)
|
|
if tc.expectErr != "" {
|
|
require.ErrorContains(t, err, tc.expectErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expected, env)
|
|
})
|
|
}
|
|
}
|