Skip to content

Commit

Permalink
Fix argocd_project v0 to v1 schema migration edge case (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
oboukili authored Jan 30, 2021
1 parent b156f19 commit c968929
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 99 deletions.
83 changes: 43 additions & 40 deletions argocd/schema_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,51 +351,54 @@ func resourceArgoCDProjectV0() *schema.Resource {
}

func resourceArgoCDProjectStateUpgradeV0(rawState map[string]interface{}, _ interface{}) (map[string]interface{}, error) {
orphanedResources := rawState["spec"].([]map[string]interface{})[0]["orphaned_resources"]
spec := rawState["spec"].([]map[string]interface{})
if len(spec) > 0 {
if orphanedResources, ok := spec[0]["orphaned_resources"]; ok {
switch orphanedResources.(type) {

switch orphanedResources.(type) {

// <= v0.4.8
case map[string]bool:
warn := orphanedResources.(map[string]bool)["warn"]
newOrphanedResources := schema.NewSet(
schema.HashResource(&schema.Resource{
Schema: map[string]*schema.Schema{
"warn": {
Type: schema.TypeBool,
Optional: true,
},
"ignore": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"group": {
Type: schema.TypeString,
ValidateFunc: validateGroupName,
Optional: true,
},
"kind": {
Type: schema.TypeString,
Optional: true,
},
"name": {
Type: schema.TypeString,
Optional: true,
// <= v0.4.8
case map[string]bool:
warn := orphanedResources.(map[string]bool)["warn"]
newOrphanedResources := schema.NewSet(
schema.HashResource(&schema.Resource{
Schema: map[string]*schema.Schema{
"warn": {
Type: schema.TypeBool,
Optional: true,
},
"ignore": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"group": {
Type: schema.TypeString,
ValidateFunc: validateGroupName,
Optional: true,
},
"kind": {
Type: schema.TypeString,
Optional: true,
},
"name": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
},
},
}),
[]interface{}{map[string]interface{}{"warn": warn}},
)
rawState["spec"].([]map[string]interface{})[0]["orphaned_resources"] = newOrphanedResources
}),
[]interface{}{map[string]interface{}{"warn": warn}},
)
rawState["spec"].([]map[string]interface{})[0]["orphaned_resources"] = newOrphanedResources

// >= v0.5.0 <= v1.1.0
case *schema.Set:
default:
return nil, fmt.Errorf("error during state migration v0 to v1, unsupported type for 'orphaned_resources': %s", orphanedResources)
// >= v0.5.0 <= v1.1.0
case *schema.Set:
default:
return nil, fmt.Errorf("error during state migration v0 to v1, unsupported type for 'orphaned_resources': %s", orphanedResources)
}
}
}
return rawState, nil
}
226 changes: 167 additions & 59 deletions argocd/schema_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,197 @@ package argocd

import (
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"reflect"
"testing"
)

func testResourceArgoCDProjectStateDataV0() map[string]interface{} {
return map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": map[string]bool{"warn": true},
func orphanedResourcesSchemaSetFuncV1() schema.SchemaSetFunc {
return schema.HashResource(&schema.Resource{
Schema: map[string]*schema.Schema{
"warn": {
Type: schema.TypeBool,
Optional: true,
},
"ignore": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"group": {
Type: schema.TypeString,
ValidateFunc: validateGroupName,
Optional: true,
},
"kind": {
Type: schema.TypeString,
Optional: true,
},
"name": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
}
})
}

func testResourceArgoCDProjectStateDataV1() map[string]interface{} {
newOrphanedResources := schema.NewSet(
schema.HashResource(&schema.Resource{
Schema: map[string]*schema.Schema{
"warn": {
Type: schema.TypeBool,
Optional: true,
func TestResourceArgoCDProjectStateUpgradeV0(t *testing.T) {
type projectStateUpgradeTestCases []struct {
name string
expectedState map[string]interface{}
sourceState map[string]interface{}
}

cases := projectStateUpgradeTestCases{
{
name: "source_<_v0.5.0_with_warn",
sourceState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": map[string]bool{"warn": true},
},
},
"ignore": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"group": {
Type: schema.TypeString,
ValidateFunc: validateGroupName,
Optional: true,
},
"kind": {
Type: schema.TypeString,
Optional: true,
},
"name": {
Type: schema.TypeString,
Optional: true,
},
},
},
expectedState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": schema.NewSet(
orphanedResourcesSchemaSetFuncV1(),
[]interface{}{map[string]interface{}{"warn": true}},
),
},
},
},
},
{
name: "source_<_v0.5.0_without_orphaned_resources",
sourceState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"source_repos": []string{"*"},
},
},
},
}),
[]interface{}{map[string]interface{}{"warn": true}},
)
return map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": newOrphanedResources,
expectedState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"source_repos": []string{"*"},
},
},
},
},
}
}

func TestResourceArgoCDProjectStateUpgradeV0(t *testing.T) {
cases := []struct {
name string
expected map[string]interface{}
sourceState map[string]interface{}
}{
{
"source < v0.5.0",
testResourceArgoCDProjectStateDataV1(),
testResourceArgoCDProjectStateDataV0(),
name: "source_<_v0.5.0_with_empty_orphaned_resources",
sourceState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": map[string]bool{},
},
},
},
expectedState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": schema.NewSet(
orphanedResourcesSchemaSetFuncV1(),
[]interface{}{map[string]interface{}{"warn": false}},
),
},
},
},
},
{
name: "source_<_v1.1.0_>=_0.4.8_with_warn",
sourceState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": schema.NewSet(
orphanedResourcesSchemaSetFuncV1(),
[]interface{}{map[string]interface{}{"warn": true}},
),
"source_repos": []string{"*"},
},
},
},
expectedState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"orphaned_resources": schema.NewSet(
orphanedResourcesSchemaSetFuncV1(),
[]interface{}{map[string]interface{}{"warn": true}},
),
"source_repos": []string{"*"},
},
},
},
},
{
"source < v1.1.0, >= v0.5.0",
testResourceArgoCDProjectStateDataV1(),
testResourceArgoCDProjectStateDataV1(),
name: "source_<_v1.1.1_without_orphaned_resources",
sourceState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"source_repos": []string{"*"},
},
},
},
expectedState: map[string]interface{}{
"spec": []map[string]interface{}{
{
"source_repos": []string{"*"},
},
},
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_actual, err := resourceArgoCDProjectStateUpgradeV0(tc.sourceState, nil)
actualState, err := resourceArgoCDProjectStateUpgradeV0(tc.sourceState, nil)
if err != nil {
t.Fatalf("error migrating state: %s", err)
}
expected := tc.expected["spec"].([]map[string]interface{})[0]["orphaned_resources"].(*schema.Set)
actual := _actual["spec"].([]map[string]interface{})[0]["orphaned_resources"].(*schema.Set)
if !expected.HashEqual(actual) {
t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual)
if !reflect.DeepEqual(actualState, tc.expectedState) {
if expectedSet, ok := tc.expectedState["spec"].([]map[string]interface{})[0]["orphaned_resources"]; ok {

actualSet := actualState["spec"].([]map[string]interface{})[0]["orphaned_resources"].(*schema.Set)

if !expectedSet.(*schema.Set).HashEqual(actualSet) {
t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expectedSet, actualSet)
}
// Cannot DeepEqual a pointer reference
for k, _ := range tc.expectedState["spec"].([]map[string]interface{})[0] {
av := actualState["spec"].([]map[string]interface{})[0][k]
ev := tc.expectedState["spec"].([]map[string]interface{})[0][k]
if k != "orphaned_resources" && !reflect.DeepEqual(av, ev) {
t.Fatalf("\n\n[maps] expected:\n\n%#v\n\ngot:\n\n%#v\n\n", tc.expectedState, actualState)
}
}
for k, av := range actualState["spec"].([]map[string]interface{})[0] {
ev := tc.expectedState["spec"].([]map[string]interface{})[0][k]
if k != "orphaned_resources" && !reflect.DeepEqual(av, ev) {
t.Fatalf("\n\n[maps] expected:\n\n%#v\n\ngot:\n\n%#v\n\n", tc.expectedState, actualState)
}
}
} else {
// Cannot DeepEqual a pointer reference
for k, _ := range tc.expectedState["spec"].([]map[string]interface{})[0] {
av := actualState["spec"].([]map[string]interface{})[0][k]
ev := tc.expectedState["spec"].([]map[string]interface{})[0][k]
if k != "orphaned_resources" && !reflect.DeepEqual(av, ev) {
t.Fatalf("\n\n[maps without set] expected:\n\n%#v\n\ngot:\n\n%#v\n\n", tc.expectedState, actualState)
}
}
for k, av := range actualState["spec"].([]map[string]interface{})[0] {
ev := tc.expectedState["spec"].([]map[string]interface{})[0][k]
if k != "orphaned_resources" && !reflect.DeepEqual(av, ev) {
t.Fatalf("\n\n[maps] expected:\n\n%#v\n\ngot:\n\n%#v\n\n", tc.expectedState, actualState)
}
}
}
}

})
}
}

0 comments on commit c968929

Please sign in to comment.