diff --git a/google/provider.go b/google/provider.go index 9f887627077..393b6fd0363 100644 --- a/google/provider.go +++ b/google/provider.go @@ -381,9 +381,9 @@ func Provider() terraform.ResourceProvider { } } -// Generated resources: 70 +// Generated resources: 71 // Generated IAM resources: 6 -// Total generated resources: 76 +// Total generated resources: 77 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap 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..ae651492215 100644 --- a/google/resource_cloudiot_registry_test.go +++ b/google/resource_cloudiot_registry_test.go @@ -2,13 +2,118 @@ package google import ( "fmt" + "reflect" "testing" + "strings" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) +func TestValidateCloudIoTID(t *testing.T) { + x := []StringValidationTestCase{ + // No errors + {TestName: "basic", Value: "foobar"}, + {TestName: "with numbers", Value: "foobar123"}, + {TestName: "short", Value: "foo"}, + {TestName: "long", Value: "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"}, + {TestName: "has a hyphen", Value: "foo-bar"}, + + // With errors + {TestName: "empty", Value: "", ExpectError: true}, + {TestName: "starts with a goog", Value: "googfoobar", ExpectError: true}, + {TestName: "starts with a number", Value: "1foobar", ExpectError: true}, + {TestName: "has an slash", Value: "foo/bar", ExpectError: true}, + {TestName: "has an backslash", Value: "foo\bar", ExpectError: true}, + {TestName: "too long", Value: strings.Repeat("f", 260), ExpectError: true}, + } + + es := testStringValidationCases(x, validateCloudIotID) + if len(es) > 0 { + t.Errorf("Failed to validate CloudIoT ID names: %v", es) + } +} + +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 +198,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 +301,8 @@ resource "google_cloudiot_registry" "foobar" { mqtt_config = { mqtt_enabled_state = "MQTT_DISABLED" } + + log_level = "INFO" credentials { public_key_certificate = { @@ -177,3 +313,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) { diff --git a/google/validation_test.go b/google/validation_test.go index d91a58d58d7..b87de14df90 100644 --- a/google/validation_test.go +++ b/google/validation_test.go @@ -217,30 +217,6 @@ func TestProjectRegex(t *testing.T) { } } -func TestValidateCloudIoTID(t *testing.T) { - x := []StringValidationTestCase{ - // No errors - {TestName: "basic", Value: "foobar"}, - {TestName: "with numbers", Value: "foobar123"}, - {TestName: "short", Value: "foo"}, - {TestName: "long", Value: "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"}, - {TestName: "has a hyphen", Value: "foo-bar"}, - - // With errors - {TestName: "empty", Value: "", ExpectError: true}, - {TestName: "starts with a goog", Value: "googfoobar", ExpectError: true}, - {TestName: "starts with a number", Value: "1foobar", ExpectError: true}, - {TestName: "has an slash", Value: "foo/bar", ExpectError: true}, - {TestName: "has an backslash", Value: "foo\bar", ExpectError: true}, - {TestName: "too long", Value: strings.Repeat("f", 260), ExpectError: true}, - } - - es := testStringValidationCases(x, validateCloudIoTID) - if len(es) > 0 { - t.Errorf("Failed to validate CloudIoT ID names: %v", es) - } -} - func TestOrEmpty(t *testing.T) { cases := map[string]struct { Value string diff --git a/website/google.erb b/website/google.erb index 49fe163dc84..72f249d3293 100644 --- a/website/google.erb +++ b/website/google.erb @@ -758,8 +758,8 @@