From 81df2d84dedbe135ccb61570fa93300e0f57a89d Mon Sep 17 00:00:00 2001 From: emily Date: Sat, 17 Aug 2019 00:10:00 +0000 Subject: [PATCH] Add log_level, plural event_notification_configs to IoT registry Signed-off-by: Modular Magician --- google/resource_cloudiot_registry.go | 202 ++++++++++++++++++++-- google/resource_cloudiot_registry_test.go | 157 +++++++++++++++++ google/validation.go | 13 -- 3 files changed, 347 insertions(+), 25 deletions(-) diff --git a/google/resource_cloudiot_registry.go b/google/resource_cloudiot_registry.go index 0af43cb065a..1e1e80462ad 100644 --- a/google/resource_cloudiot_registry.go +++ b/google/resource_cloudiot_registry.go @@ -2,11 +2,12 @@ package google import ( "fmt" + "github.com/hashicorp/terraform/helper/validation" + "log" "regexp" "strings" "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/helper/validation" "google.golang.org/api/cloudiot/v1" ) @@ -29,12 +30,21 @@ func resourceCloudIoTRegistry() *schema.Resource { State: resourceCloudIoTRegistryStateImporter, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: resourceCloudIotRegistryV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceCloudIotRegistryStateUpgradeV0toV1, + }, + }, + Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: validateCloudIoTID, + ValidateFunc: validateCloudIotID, }, "project": { Type: schema.TypeString, @@ -48,9 +58,19 @@ func resourceCloudIoTRegistry() *schema.Resource { Computed: true, ForceNew: true, }, + "log_level": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: emptyOrDefaultStringSuppress(""), + ValidateFunc: validation.StringInSlice( + []string{"", "NONE", "ERROR", "INFO", "DEBUG"}, false), + }, "event_notification_config": { - Type: schema.TypeMap, - Optional: true, + Type: schema.TypeMap, + Optional: true, + Computed: true, + Deprecated: "eventNotificationConfig has been deprecated in favor or eventNotificationConfigs (plural). Please switch to using the plural field.", + ConflictsWith: []string{"event_notification_configs"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "pubsub_topic_name": { @@ -61,6 +81,27 @@ func resourceCloudIoTRegistry() *schema.Resource { }, }, }, + "event_notification_configs": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 10, + ConflictsWith: []string{"event_notification_config"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pubsub_topic_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + }, + "subfolder_matches": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCloudIotRegistrySubfolderMatch, + }, + }, + }, + }, "state_notification_config": { Type: schema.TypeMap, Optional: true, @@ -135,6 +176,17 @@ func resourceCloudIoTRegistry() *schema.Resource { } } +func buildEventNotificationConfigs(v []interface{}) []*cloudiot.EventNotificationConfig { + cfgList := make([]*cloudiot.EventNotificationConfig, 0, len(v)) + for _, cfgRaw := range v { + if cfgRaw == nil { + continue + } + cfgList = append(cfgList, buildEventNotificationConfig(cfgRaw.(map[string]interface{}))) + } + return cfgList +} + func buildEventNotificationConfig(config map[string]interface{}) *cloudiot.EventNotificationConfig { if v, ok := config["pubsub_topic_name"]; ok { return &cloudiot.EventNotificationConfig{ @@ -192,10 +244,13 @@ func expandCredentials(credentials []interface{}) []*cloudiot.RegistryCredential func createDeviceRegistry(d *schema.ResourceData) *cloudiot.DeviceRegistry { deviceRegistry := &cloudiot.DeviceRegistry{} - if v, ok := d.GetOk("event_notification_config"); ok { - deviceRegistry.EventNotificationConfigs = make([]*cloudiot.EventNotificationConfig, 1, 1) - deviceRegistry.EventNotificationConfigs[0] = buildEventNotificationConfig(v.(map[string]interface{})) + if v, ok := d.GetOk("event_notification_configs"); ok { + deviceRegistry.EventNotificationConfigs = buildEventNotificationConfigs(v.([]interface{})) + } else if v, ok := d.GetOk("event_notification_config"); ok { + deviceRegistry.EventNotificationConfigs = []*cloudiot.EventNotificationConfig{ + buildEventNotificationConfig(v.(map[string]interface{}))} } + if v, ok := d.GetOk("state_notification_config"); ok { deviceRegistry.StateNotificationConfig = buildStateNotificationConfig(v.(map[string]interface{})) } @@ -208,6 +263,11 @@ func createDeviceRegistry(d *schema.ResourceData) *cloudiot.DeviceRegistry { if v, ok := d.GetOk("credentials"); ok { deviceRegistry.Credentials = expandCredentials(v.([]interface{})) } + if v, ok := d.GetOk("log_level"); ok { + deviceRegistry.LogLevel = v.(string) + } + deviceRegistry.ForceSendFields = append(deviceRegistry.ForceSendFields, "logLevel") + return deviceRegistry } @@ -251,14 +311,23 @@ func resourceCloudIoTRegistryUpdate(d *schema.ResourceData, meta interface{}) er d.Partial(true) + if d.HasChange("event_notification_configs") { + hasChanged = true + updateMask = append(updateMask, "event_notification_configs") + if v, ok := d.GetOk("event_notification_configs"); ok { + deviceRegistry.EventNotificationConfigs = buildEventNotificationConfigs(v.([]interface{})) + } + } + if d.HasChange("event_notification_config") { hasChanged = true updateMask = append(updateMask, "event_notification_configs") if v, ok := d.GetOk("event_notification_config"); ok { - deviceRegistry.EventNotificationConfigs = make([]*cloudiot.EventNotificationConfig, 1, 1) - deviceRegistry.EventNotificationConfigs[0] = buildEventNotificationConfig(v.(map[string]interface{})) + deviceRegistry.EventNotificationConfigs = []*cloudiot.EventNotificationConfig{ + buildEventNotificationConfig(v.(map[string]interface{}))} } } + if d.HasChange("state_notification_config") { hasChanged = true updateMask = append(updateMask, "state_notification_config.pubsub_topic_name") @@ -287,6 +356,14 @@ func resourceCloudIoTRegistryUpdate(d *schema.ResourceData, meta interface{}) er deviceRegistry.Credentials = expandCredentials(v.([]interface{})) } } + if d.HasChange("log_level") { + hasChanged = true + updateMask = append(updateMask, "log_level") + if v, ok := d.GetOk("log_level"); ok { + deviceRegistry.LogLevel = v.(string) + deviceRegistry.ForceSendFields = append(deviceRegistry.ForceSendFields, "logLevel") + } + } if hasChanged { _, err := config.clientCloudIoT.Projects.Locations.Registries.Patch(d.Id(), deviceRegistry).UpdateMask(strings.Join(updateMask, ",")).Do() @@ -297,10 +374,25 @@ func resourceCloudIoTRegistryUpdate(d *schema.ResourceData, meta interface{}) er d.SetPartial(updateMaskItem) } } + d.Partial(false) return resourceCloudIoTRegistryRead(d, meta) } +func flattenCloudIotRegistryEventNotificationConfigs(cfgs []*cloudiot.EventNotificationConfig, d *schema.ResourceData) []interface{} { + ls := make([]interface{}, 0, len(cfgs)) + for _, cfg := range cfgs { + if cfg == nil { + continue + } + ls = append(ls, map[string]interface{}{ + "subfolder_matches": cfg.SubfolderMatches, + "pubsub_topic_name": cfg.PubsubTopicName, + }) + } + return ls +} + func resourceCloudIoTRegistryRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) name := d.Id() @@ -308,15 +400,23 @@ func resourceCloudIoTRegistryRead(d *schema.ResourceData, meta interface{}) erro if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("Registry %q", name)) } - d.Set("name", res.Id) if len(res.EventNotificationConfigs) > 0 { - eventConfig := map[string]string{"pubsub_topic_name": res.EventNotificationConfigs[0].PubsubTopicName} - d.Set("event_notification_config", eventConfig) + cfgs := flattenCloudIotRegistryEventNotificationConfigs(res.EventNotificationConfigs, d) + if err := d.Set("event_notification_configs", cfgs); err != nil { + return fmt.Errorf("Error reading Registry: %s", err) + } + if err := d.Set("event_notification_config", map[string]string{ + "pubsub_topic_name": res.EventNotificationConfigs[0].PubsubTopicName, + }); err != nil { + return fmt.Errorf("Error reading Registry: %s", err) + } } else { + d.Set("event_notification_configs", nil) d.Set("event_notification_config", nil) } + pubsubTopicName := res.StateNotificationConfig.PubsubTopicName if pubsubTopicName != "" { d.Set("state_notification_config", @@ -337,6 +437,8 @@ func resourceCloudIoTRegistryRead(d *schema.ResourceData, meta interface{}) erro credentials[i]["public_key_certificate"] = pubcert } d.Set("credentials", credentials) + d.Set("log_level", res.LogLevel) + return nil } @@ -369,3 +471,79 @@ func resourceCloudIoTRegistryStateImporter(d *schema.ResourceData, meta interfac d.SetId(id) return []*schema.ResourceData{d}, nil } + +func resourceCloudIotRegistryStateUpgradeV0toV1(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + log.Printf("[DEBUG] State before upgrade: %#v", rawState) + + oldCfg, ok := rawState["event_notification_config"] + if ok { + delete(rawState, "event_notification_config") + } + + v, ok := rawState["event_notification_configs"] + if ok && v != nil { + // Just ignore old value if event_notification_configs is already set. + return rawState, nil + } + + rawState["event_notification_configs"] = []interface{}{oldCfg} + log.Printf("[DEBUG] State after upgrade: %#v", rawState) + return rawState, nil +} + +func validateCloudIotID(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + if strings.HasPrefix(value, "goog") { + errors = append(errors, fmt.Errorf( + "%q (%q) can not start with \"goog\"", k, value)) + } + if !regexp.MustCompile(CloudIoTIdRegex).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q (%q) doesn't match regexp %q", k, value, CloudIoTIdRegex)) + } + return +} + +func validateCloudIotRegistrySubfolderMatch(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + if strings.HasPrefix(value, "/") { + errors = append(errors, fmt.Errorf( + "%q (%q) can not start with '/'", k, value)) + } + return +} + +func resourceCloudIotRegistryV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "log_level": { + Type: schema.TypeString, + Optional: true, + }, + "event_notification_config": { + Type: schema.TypeMap, + Optional: true, + }, + "state_notification_config": { + Type: schema.TypeMap, + Optional: true, + }, + "mqtt_config": { + Type: schema.TypeMap, + Optional: true, + }, + "http_config": { + Type: schema.TypeMap, + Optional: true, + }, + "credentials": { + Type: schema.TypeList, + Optional: true, + }, + }, + } +} diff --git a/google/resource_cloudiot_registry_test.go b/google/resource_cloudiot_registry_test.go index 6eacd4acf74..bdb2b987eb1 100644 --- a/google/resource_cloudiot_registry_test.go +++ b/google/resource_cloudiot_registry_test.go @@ -2,6 +2,7 @@ package google import ( "fmt" + "reflect" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -9,6 +10,85 @@ import ( "github.com/hashicorp/terraform/terraform" ) +func TestCloudIotRegistryStateUpgradeV0(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + V0State map[string]interface{} + V1Expected map[string]interface{} + }{ + "Move single to plural": { + V0State: map[string]interface{}{ + "event_notification_config": map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/my-topic", + }, + }, + V1Expected: map[string]interface{}{ + "event_notification_configs": []interface{}{ + map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/my-topic", + }, + }, + }, + }, + "Delete single if plural in state": { + V0State: map[string]interface{}{ + "event_notification_config": map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/singular-topic", + }, + "event_notification_configs": []interface{}{ + map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/plural-topic", + }, + }, + }, + V1Expected: map[string]interface{}{ + "event_notification_configs": []interface{}{ + map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/plural-topic", + }, + }, + }, + }, + "no-op": { + V0State: map[string]interface{}{ + "name": "my-test-name", + "log_level": "INFO", + "event_notification_configs": []interface{}{ + map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/plural-topic", + }, + }, + }, + V1Expected: map[string]interface{}{ + "name": "my-test-name", + "log_level": "INFO", + "event_notification_configs": []interface{}{ + map[string]interface{}{ + "pubsub_topic_name": "projects/my-project/topics/plural-topic", + }, + }, + }, + }, + } + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + actual, err := resourceCloudIotRegistryStateUpgradeV0toV1(tc.V0State, &Config{}) + + if err != nil { + t.Error(err) + } + + for k, v := range tc.V1Expected { + if !reflect.DeepEqual(actual[k], v) { + t.Errorf("expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v", + k, v, k, actual[k], actual) + } + } + }) + } +} + func TestAccCloudIoTRegistry_basic(t *testing.T) { t.Parallel() @@ -93,6 +173,35 @@ func TestAccCloudIoTRegistry_update(t *testing.T) { }) } +func TestAccCloudIoTRegistry_deprecatedEventNotificationConfig(t *testing.T) { + t.Parallel() + + registryName := fmt.Sprintf("tf-registry-test-%s", acctest.RandString(10)) + topic := fmt.Sprintf("tf-registry-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudIoTRegistryDestroy, + Steps: []resource.TestStep{ + { + // Use deprecated field (event_notification_config) to create + Config: testAccCloudIoTRegistry_singleEventNotificationConfig(topic, registryName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "google_cloudiot_registry.foobar", "event_notification_configs.#", "1"), + ), + }, + { + // Use new field (event_notification_configs) to see if plan changed + Config: testAccCloudIoTRegistry_pluralEventNotificationConfigs(topic, registryName), + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) +} + func testAccCheckCloudIoTRegistryDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { if rs.Type != "google_cloudiot_registry" { @@ -167,6 +276,8 @@ resource "google_cloudiot_registry" "foobar" { mqtt_config = { mqtt_enabled_state = "MQTT_DISABLED" } + + log_level = "INFO" credentials { public_key_certificate = { @@ -177,3 +288,49 @@ resource "google_cloudiot_registry" "foobar" { } `, acctest.RandString(10), acctest.RandString(10), registryName) } + +func testAccCloudIoTRegistry_singleEventNotificationConfig(topic, registryName string) string { + return fmt.Sprintf(` +resource "google_project_iam_binding" "cloud-iot-iam-binding" { + members = ["serviceAccount:cloud-iot@system.gserviceaccount.com"] + role = "roles/pubsub.publisher" +} + +resource "google_pubsub_topic" "event-topic" { + name = "%s" +} + +resource "google_cloudiot_registry" "foobar" { + depends_on = ["google_project_iam_binding.cloud-iot-iam-binding"] + + name = "%s" + + event_notification_config = { + pubsub_topic_name = "${google_pubsub_topic.event-topic.id}" + } +} +`, topic, registryName) +} + +func testAccCloudIoTRegistry_pluralEventNotificationConfigs(topic, registryName string) string { + return fmt.Sprintf(` +resource "google_project_iam_binding" "cloud-iot-iam-binding" { + members = ["serviceAccount:cloud-iot@system.gserviceaccount.com"] + role = "roles/pubsub.publisher" +} + +resource "google_pubsub_topic" "event-topic" { + name = "%s" +} + +resource "google_cloudiot_registry" "foobar" { + depends_on = ["google_project_iam_binding.cloud-iot-iam-binding"] + + name = "%s" + + event_notification_config = { + pubsub_topic_name = "${google_pubsub_topic.event-topic.id}" + } +} +`, topic, registryName) +} diff --git a/google/validation.go b/google/validation.go index 480319aaeda..b10ceb778fe 100644 --- a/google/validation.go +++ b/google/validation.go @@ -145,19 +145,6 @@ func validateIpCidrRange(v interface{}, k string) (warnings []string, errors []e return } -func validateCloudIoTID(v interface{}, k string) (warnings []string, errors []error) { - value := v.(string) - if strings.HasPrefix(value, "goog") { - errors = append(errors, fmt.Errorf( - "%q (%q) can not start with \"goog\"", k, value)) - } - if !regexp.MustCompile(CloudIoTIdRegex).MatchString(value) { - errors = append(errors, fmt.Errorf( - "%q (%q) doesn't match regexp %q", k, value, CloudIoTIdRegex)) - } - return -} - func validateIAMCustomRoleID(v interface{}, k string) (warnings []string, errors []error) { value := v.(string) if !regexp.MustCompile(IAMCustomRoleIDRegex).MatchString(value) {