mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
Closes https://github.com/coder/internal/issues/369 We can't know whether a replacement (i.e. drift of terraform state leading to a resource needing to be deleted/recreated) will take place apriori; we can only detect it at `plan` time, because the provider decides whether a resource must be replaced and it cannot be inferred through static analysis of the template. **This is likely to be the most common gotcha with using prebuilds, since it requires a slight template modification to use prebuilds effectively**, so let's head this off before it's an issue for customers. Drift details will now be logged in the workspace build logs:  Plus a notification will be sent to template admins when this situation arises:  A new metric - `coderd_prebuilt_workspaces_resource_replacements_total` - will also increment each time a workspace encounters replacements. We only track _that_ a resource replacement occurred, not how many. Just one is enough to ruin a prebuild, but we can't know apriori which replacement would cause this. For example, say we have 2 replacements: a `docker_container` and a `null_resource`; we don't know which one might cause an issue (or indeed if either would), so we just track the replacement. --------- Signed-off-by: Danny Kopping <dannykopping@gmail.com>
177 lines
3.9 KiB
Go
177 lines
3.9 KiB
Go
package terraform
|
|
|
|
import (
|
|
"testing"
|
|
|
|
tfjson "github.com/hashicorp/terraform-json"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestFindResourceReplacements(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
plan *tfjson.Plan
|
|
expected resourceReplacements
|
|
}{
|
|
{
|
|
name: "nil plan",
|
|
},
|
|
{
|
|
name: "no resource changes",
|
|
plan: &tfjson.Plan{},
|
|
},
|
|
{
|
|
name: "resource change with nil change",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "no-op action",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionNoop},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty replace paths",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "coder_* types are ignored",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Type: "coder_resource",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{"path1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "valid replacements - single path",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Type: "example_resource",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{"path1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: resourceReplacements{
|
|
"resource1": {"path1"},
|
|
},
|
|
},
|
|
{
|
|
name: "valid replacements - multiple paths",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Type: "example_resource",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{"path1", "path2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: resourceReplacements{
|
|
"resource1": {"path1", "path2"},
|
|
},
|
|
},
|
|
{
|
|
name: "complex replace path",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Type: "example_resource",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{
|
|
[]interface{}{"path", "to", "key"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: resourceReplacements{
|
|
"resource1": {"path.to.key"},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple changes",
|
|
plan: &tfjson.Plan{
|
|
ResourceChanges: []*tfjson.ResourceChange{
|
|
{
|
|
Address: "resource1",
|
|
Type: "example_resource",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{"path1"},
|
|
},
|
|
},
|
|
{
|
|
Address: "resource2",
|
|
Type: "example_resource",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{"path2", "path3"},
|
|
},
|
|
},
|
|
{
|
|
Address: "resource3",
|
|
Type: "coder_example",
|
|
Change: &tfjson.Change{
|
|
Actions: tfjson.Actions{tfjson.ActionDelete, tfjson.ActionCreate},
|
|
ReplacePaths: []interface{}{"ignored_path"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: resourceReplacements{
|
|
"resource1": {"path1"},
|
|
"resource2": {"path2", "path3"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.EqualValues(t, tc.expected, findResourceReplacements(tc.plan))
|
|
})
|
|
}
|
|
}
|