From b156f19cade82f7492cc3a6859d0378e2eb02bdc Mon Sep 17 00:00:00 2001 From: Olivier Boukili Date: Fri, 29 Jan 2021 04:31:09 +0100 Subject: [PATCH] argocd_project: introduce schema versioning and migration (#58) --- argocd/resource_argocd_project.go | 10 +- argocd/schema_project.go | 217 +++++++++++++++++++++++++++++- argocd/schema_project_test.go | 90 +++++++++++++ 3 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 argocd/schema_project_test.go diff --git a/argocd/resource_argocd_project.go b/argocd/resource_argocd_project.go index 2207ee40..c84184f1 100644 --- a/argocd/resource_argocd_project.go +++ b/argocd/resource_argocd_project.go @@ -23,7 +23,15 @@ func resourceArgoCDProject() *schema.Resource { }, Schema: map[string]*schema.Schema{ "metadata": metadataSchema("appprojects.argoproj.io"), - "spec": projectSpecSchema(), + "spec": projectSpecSchemaV1(), + }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceArgoCDProjectV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceArgoCDProjectStateUpgradeV0, + Version: 0, + }, }, } } diff --git a/argocd/schema_project.go b/argocd/schema_project.go index d6721734..2c9ecd66 100644 --- a/argocd/schema_project.go +++ b/argocd/schema_project.go @@ -1,8 +1,162 @@ package argocd -import "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) -func projectSpecSchema() *schema.Schema { +func projectSpecSchemaV0() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Description: "ArgoCD App project resource specs. Required attributes: destination, source_repos.", + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cluster_resource_whitelist": { + 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, + }, + }, + }, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "destination": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "server": { + Type: schema.TypeString, + Optional: true, + }, + "namespace": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the destination cluster which can be used instead of server.", + }, + }, + }, + }, + "namespace_resource_blacklist": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "group": { + Type: schema.TypeString, + Optional: true, + }, + "kind": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "orphaned_resources": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeBool}, + }, + "role": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "description": { + Type: schema.TypeString, + Optional: true, + }, + "groups": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name": { + Type: schema.TypeString, + ValidateFunc: validateRoleName, + Required: true, + }, + "policies": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "source_repos": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "sync_window": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "applications": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "clusters": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "duration": { + Type: schema.TypeString, + ValidateFunc: validateSyncWindowDuration, + Optional: true, + }, + "kind": { + Type: schema.TypeString, + ValidateFunc: validateSyncWindowKind, + Optional: true, + }, + "manual_sync": { + Type: schema.TypeBool, + Optional: true, + }, + "namespaces": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "schedule": { + Type: schema.TypeString, + ValidateFunc: validateSyncWindowSchedule, + Optional: true, + }, + }, + }, + }, + }, + }, + } +} + +func projectSpecSchemaV1() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, MinItems: 1, @@ -186,3 +340,62 @@ func projectSpecSchema() *schema.Schema { }, } } + +func resourceArgoCDProjectV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metadata": metadataSchema("appprojects.argoproj.io"), + "spec": projectSpecSchemaV0(), + }, + } +} + +func resourceArgoCDProjectStateUpgradeV0(rawState map[string]interface{}, _ interface{}) (map[string]interface{}, error) { + orphanedResources := rawState["spec"].([]map[string]interface{})[0]["orphaned_resources"] + + 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, + }, + }, + }, + }, + }, + }), + []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) + } + return rawState, nil +} diff --git a/argocd/schema_project_test.go b/argocd/schema_project_test.go new file mode 100644 index 00000000..055dfc41 --- /dev/null +++ b/argocd/schema_project_test.go @@ -0,0 +1,90 @@ +package argocd + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "testing" +) + +func testResourceArgoCDProjectStateDataV0() map[string]interface{} { + return map[string]interface{}{ + "spec": []map[string]interface{}{ + { + "orphaned_resources": map[string]bool{"warn": true}, + }, + }, + } +} + +func testResourceArgoCDProjectStateDataV1() map[string]interface{} { + 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": true}}, + ) + return map[string]interface{}{ + "spec": []map[string]interface{}{ + { + "orphaned_resources": newOrphanedResources, + }, + }, + } +} + +func TestResourceArgoCDProjectStateUpgradeV0(t *testing.T) { + cases := []struct { + name string + expected map[string]interface{} + sourceState map[string]interface{} + }{ + { + "source < v0.5.0", + testResourceArgoCDProjectStateDataV1(), + testResourceArgoCDProjectStateDataV0(), + }, + { + "source < v1.1.0, >= v0.5.0", + testResourceArgoCDProjectStateDataV1(), + testResourceArgoCDProjectStateDataV1(), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _actual, 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) + } + }) + } +}