From 55c5ab5d97389119f99c07a24305662d067a53fd Mon Sep 17 00:00:00 2001 From: tagur87 <43474056+tagur87@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:25:39 -0400 Subject: [PATCH] feat: allow any custom field type in netbox_device Updates the `netbox_device` resource to allow for any custom field type to be set. This is done by using a string field with the `jsonencode()` and `jsondecode()` terraform functions. This allows for any complex custom fields or data types that are needed, since the underlying types don't matter and are unmarshalled using map[string]interface{}. To set custom fields using this new method, use the following syntax. ```terraform resource "netbox_device" "test" { name = "device1" tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id custom_fields = jsonencode({ "text_field" = "81" "bool_field" = true }) } ``` NOTE: This may be a breaking change for some users. --- docs/resources/device.md | 1 + netbox/resource_netbox_device.go | 26 +++++++---- netbox/resource_netbox_device_test.go | 67 +++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/docs/resources/device.md b/docs/resources/device.md index 57ff4f1e..d40b972f 100644 --- a/docs/resources/device.md +++ b/docs/resources/device.md @@ -58,6 +58,7 @@ resource "netbox_device" "test" { - `comments` (String) - `custom_fields` (Map of String) - `description` (String) +- `custom_fields` (String) A JSON string that defines the custom fields as defined under the `custom_fields` key in the object's api.This is best managed with the `jsonencode()` & `jsondecode()` functions. - `location_id` (Number) - `platform_id` (Number) - `rack_face` (String) Valid values are `front` and `rear`. Required when `rack_position` is set. diff --git a/netbox/resource_netbox_device.go b/netbox/resource_netbox_device.go index de97f138..0a43a74d 100644 --- a/netbox/resource_netbox_device.go +++ b/netbox/resource_netbox_device.go @@ -2,6 +2,7 @@ package netbox import ( "context" + "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client" @@ -102,7 +103,7 @@ func resourceNetboxDevice() *schema.Resource { Type: schema.TypeFloat, Optional: true, }, - customFieldsKey: customFieldsSchema, + customFieldsKey: customFieldsSchemaFunc(), }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -179,9 +180,14 @@ func resourceNetboxDeviceCreate(ctx context.Context, d *schema.ResourceData, m i data.Position = nil } - ct, ok := d.GetOk(customFieldsKey) + cf, ok := d.GetOk(customFieldsKey) if ok { - data.CustomFields = ct + var cfMap map[string]interface{} + err := json.Unmarshal([]byte(cf.(string)), &cfMap) + if err != nil { + return diag.Errorf("error in resourceNetboxDeviceCreate[CustomFields]: %v", err) + } + data.CustomFields = cfMap } data.Tags, _ = getNestedTagListFromResourceDataSet(api, d.Get(tagsKey)) @@ -276,10 +282,11 @@ func resourceNetboxDeviceRead(ctx context.Context, d *schema.ResourceData, m int d.Set("site_id", nil) } - cf := getCustomFields(res.GetPayload().CustomFields) - if cf != nil { - d.Set(customFieldsKey, cf) + cf, err := handleCustomFieldRead(device.CustomFields) + if err != nil { + return diag.FromErr(err) } + d.Set(customFieldsKey, cf) d.Set("comments", device.Comments) @@ -377,8 +384,11 @@ func resourceNetboxDeviceUpdate(ctx context.Context, d *schema.ResourceData, m i data.Face = getOptionalStr(d, "rack_face", false) data.Position = getOptionalFloat(d, "rack_position") - cf, ok := d.GetOk(customFieldsKey) - if ok { + if d.HasChange(customFieldsKey) { + cf, err := handleCustomFieldUpdate(d.GetChange(customFieldsKey)) + if err != nil { + return diag.Errorf("error in resourceNetboxDeviceUpdate[CustomFields]: %v", err) + } data.CustomFields = cf } diff --git a/netbox/resource_netbox_device_test.go b/netbox/resource_netbox_device_test.go index 5bf5e680..f9a0e6a8 100644 --- a/netbox/resource_netbox_device_test.go +++ b/netbox/resource_netbox_device_test.go @@ -210,6 +210,73 @@ resource "netbox_device" "test" { }) } +func TestAccNetboxDevice_CustomFields(t *testing.T) { + testSlug := "device_basic" + testName := testAccGetTestName(testSlug) + testField := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDeviceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` +resource "netbox_custom_field" "text" { + name = "%[1]s_text" + type = "text" + content_types = ["dcim.device"] +} +resource "netbox_custom_field" "boolean" { + name = "%[1]s_boolean" + type = "boolean" + content_types = ["dcim.device"] +} +resource "netbox_device" "test" { + name = "%[2]s" + tenant_id = netbox_tenant.test.id + role_id = netbox_device_role.test.id + device_type_id = netbox_device_type.test.id + site_id = netbox_site.test.id + custom_fields = jsonencode({ + "${netbox_custom_field.text.name}" = "81" + "${netbox_custom_field.boolean.name}" = true + }) +}`, testField, testName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netbox_device.test", "name", testName), + resource.TestCheckResourceAttrPair("netbox_device.test", "tenant_id", "netbox_tenant.test", "id"), + resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), + resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), + resource.TestCheckResourceAttr("netbox_device.test", "custom_fields", "{\""+testField+"_boolean\":true,\""+testField+"_text\":\"81\"}"), + ), + }, + { + Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` +resource "netbox_custom_field" "text" { + name = "%[1]s_text" + type = "text" + content_types = ["dcim.device"] +} +resource "netbox_custom_field" "boolean" { + name = "%[1]s_boolean" + type = "boolean" + content_types = ["dcim.device"] +} +resource "netbox_device" "test" { + name = "%[2]s" + tenant_id = netbox_tenant.test.id + role_id = netbox_device_role.test.id + device_type_id = netbox_device_type.test.id + site_id = netbox_site.test.id +}`, testField, testName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netbox_device.test", "custom_fields", ""), + ), + }, + }, + }) +} + func testAccCheckDeviceDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*client.NetBoxAPI)