diff --git a/.gitignore b/.gitignore index e77e190..9d64491 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ terraform-provider-phpipam pkg/ +tf.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cf5218..483121b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ -## 0.1.2-pre +## 0.1.3-pre Bumped version for dev. +## 0.1.2 + +Added custom field support - this plugin now supports custom fields in +addresses, subnets, and VLANs, as long as those fields are optional. Data source +searching supports addresses and subnets only, due to limitations in VLAN +searching capabilities. + ## 0.1.1 Bumping release so that I have a consistent snapshot, and also so that I can diff --git a/README.md b/README.md index 93f079f..82401a0 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ features become generally available in the PHPIPAM API, will allow lookup based on host name, allowing for better ability for this resource to discover IP addresses that have been pre-assigned for a specific resource. -Example: +**Example:** ``` data "phpipam_address" "address" { @@ -122,6 +122,33 @@ output "address_description" { } ``` +**Example With `description`:** + +``` +data "phpipam_address" "address" { + subnet_id = 3 + description_match = "Customer 1" +} + +output "address_description" { + value = "${data.phpipam_address.address.description}" +} +``` + +**Example With `custom_field_filter_key` and `custom_field_filter_value`:** + +``` +data "phpipam_address" "address" { + subnet_id = 3 + custom_field_filter_key = "CustomTestAddresses" + custom_field_filter_value = ".*terraform.*" +} + +output "address_description" { + value = "${data.phpipam_address.address.description}" +} +``` + ##### Argument Reference The data source takes the following parameters: @@ -134,27 +161,30 @@ The data source takes the following parameters: when using this field. * `hostname` - The host name of the IP address. `subnet_id` is required when using this field. - -⚠️ **NOTE:** While the `phpipam_subnet` field has a `description_match` field, the -address data source does not. The intention of `description_match` in -`phpipam_subnet` is to supply an ad-hoc tagging system where a subnet can be -assigned to multiple projects at once, which can then be searched on with this -field. This is in lieu of a custom field scheme that would support such a -system. Custom fields are not implemented in `phpipam-sdk-go`, and hence are not -implemented in this plugin - if there is enough demand for it and/or need -necessitates, this may change. - -⚠️ **NOTE:** `description` and `hostname` fields return the first match found -without any warnings. If you have multiple addresses assigned to a single host -and need to search on it, enter a unique value in the description and search on -that. IP address searches return errors on multiple results to assert that you -are getting the specific address you are looking for. + * `custom_field_filter_key` - A name of a custom field to search for. + * `custom_field_filter_value` - A regular expression to search on. The regular + expression syntax is RE2 syntax for which you can find documentation + [here](https://github.com/google/re2/wiki/Syntax). `custom_field_filter_key` + is required to use this parameter. + +⚠️ **NOTE:** `description`, `hostname`, `custom_field_filter_key`, and +`custom_field_filter_value` fields return the first match found without any +warnings. If you have multiple addresses assigned to a single host and need to +search on it, enter a unique value in the description and search on that, or use +a specific enough value in the `custom_field_filter_value` field to return a +unique match. IP address searches return errors on multiple results to assert +that you are getting the specific address you are looking for. + +⚠️ **NOTE:** An empty or unspecified `custom_field_filter_value` is the +equivalent to a regular expression that matches everything, and hence will +return the first address it sees in the subnet. Arguments are processed in the following order of precedence: * `address_id` * `ip_address` - * `subnet_id`, and either one of `description` or `hostname` + * `subnet_id`, and either one of `description`, `hostname`, or + `custom_field_filter_key` (and `custom_field_filter_value`) ##### Attribute Reference @@ -183,6 +213,7 @@ as attributes, and ones that were not supplied are populated. * `last_seen` - The last time this IP address answered ping probes. * `exclude_ping` - `true` if this address is excluded from ping probes. * `edit_date` - The last time this resource was modified. + * `custom_fields` - A key/value map of custom fields for this address. ##### The `phpipam_first_free_address` Data Source @@ -197,7 +228,7 @@ fail. Conversely, marking a subnet as unavailable or used will not prevent this data source from returning an IP address, so be aware of this while using this resource. -Example: +**Example:** ``` // Look up the subnet @@ -268,7 +299,7 @@ either by database ID or name. This data can then be used to manage other parts of PHPIPAM, such as in the event that the section name is known but not its ID, which is required for managing subnets. -Example: +**Example:** ``` data "phpipam_section" "section" { @@ -320,7 +351,7 @@ as attributes, and ones that were not supplied are populated. The `phpipam_subnet` data source gets information on a subnet such as its ID (required for creating addresses), description, and more. -Example: +**Example:** ``` // Look up the subnet @@ -338,6 +369,69 @@ resource "phpipam_address" { } ``` +**Example with `description_match`:** + +``` +// Look up the subnet (matching on either case of "customer") +data "phpipam_subnet" "subnet" { + section_id = 1 + description_match = "[Cc]ustomer 2" +} + +// Get the first available address +data "phpipam_first_free_address" "next_address" { + subnet_id = "${data.phpipam_subnet.subnet.subnet_id}" +} + +// Reserve the address. Note that we use ignore_changes here to ensure that we +// don't end up re-allocating this address on future Terraform runs. +resource "phpipam_address" { + subnet_id = "${data.phpipam_subnet.subnet.subnet_id}" + ip_address = "${data.phpipam_first_free_address.next_address.ip_address}" + hostname = "tf-test-host.example.internal" + description = "Managed by Terraform" + + lifecycle { + ignore_changes = [ + "subnet_id", + "ip_address", + ] + } +} +``` + +**Example With `custom_field_filter_key` and `custom_field_filter_value`:** + +``` +// Look up the subnet +data "phpipam_subnet" "subnet" { + section_id = 1 + custom_field_filter_key = "CustomTestSubnets" + custom_field_filter_value = ".*terraform.*" +} + +// Get the first available address +data "phpipam_first_free_address" "next_address" { + subnet_id = "${data.phpipam_subnet.subnet.subnet_id}" +} + +// Reserve the address. Note that we use ignore_changes here to ensure that we +// don't end up re-allocating this address on future Terraform runs. +resource "phpipam_address" { + subnet_id = "${data.phpipam_subnet.subnet.subnet_id}" + ip_address = "${data.phpipam_first_free_address.next_address.ip_address}" + hostname = "tf-test-host.example.internal" + description = "Managed by Terraform" + + lifecycle { + ignore_changes = [ + "subnet_id", + "ip_address", + ] + } +} +``` + ##### Argument Reference The data source takes the following parameters: @@ -351,17 +445,28 @@ The data source takes the following parameters: want to use this option. * `description_match` - A regular expression to match against when searching for a subnet. `section_id` is required if you want to use this option. - -⚠️ **NOTE:** Searches with the `description` or `description_match` fields -return the first match found without any warnings. Conversely, the resource -fails if it somehow finds multiple results on a CIDR (subnet and mask) search - -this is to assert that you are getting the subnet you requested. + * `custom_field_filter_key` - A name of a custom field to search for. + * `custom_field_filter_value` - A regular expression to search on. The regular + expression syntax is RE2 syntax for which you can find documentation + [here](https://github.com/google/re2/wiki/Syntax). `custom_field_filter_key` + is required to use this parameter. + +⚠️ **NOTE:** Searches with the `description`, `description_match`, +`custom_field_filter_key`, and `custom_field_filter_value` fields return the +first match found without any warnings. Conversely, the resource fails if it +somehow finds multiple results on a CIDR (subnet and mask) search - this is to +assert that you are getting the subnet you requested. + +⚠️ **NOTE:** An empty or unspecified `custom_field_filter_value` is the +equivalent to a regular expression that matches everything, and hence will +return the first subnetit sees in the section. Arguments are processed in the following order of precedence: * `subnet_id` * `subnet_address` and `subnet_mask` - * `section_id`, and either one of `description` or `description_match` + * `section_id`, and either one of `description`, `description_match`, or + `custom_field_filter_key` (and `custom_field_filter_value`) ##### Attribute Reference @@ -401,6 +506,7 @@ as attributes, and ones that were not supplied are populated. * `utilization_threshold` - The subnet's utilization threshold. * `location_id` - The ID of the location for this subnet. * `edit_date` - The date this resource was last updated. + * `custom_fields` - A key/value map of custom fields for this subnet. #### The `phpipam_vlan` Data Source @@ -409,7 +515,7 @@ database. This can then be used to assign a VLAN to a subnet in the `phpipam_subnet` resource. It can also be used to gather other information on the VLAN. -Example: +**Example:** ``` data "phpipam_section" "section" { @@ -451,6 +557,7 @@ as attributes, and ones that were not supplied are populated. * `name` - The name/label of the VLAN. * `description` - The description supplied to the VLAN. * `edit_date` - The date this resource was last updated. + * `custom_fields` - A key/value map of custom fields for this VLAN. ### Resources @@ -463,13 +570,15 @@ to create IP address reservations for IP addresses that have been created by other Terraform resources, or supplied by the `phpipam_first_free_address` data source. An example usage is below. -**NOTE:** If you are using the `phpipam_first_free_address` to get the first +⚠️ **NOTE:** If you are using the `phpipam_first_free_address` to get the first free IP address in a specific subnet, make sure you set `subnet_id` and `ip_address` as ignored attributes with the `ignore_changes` lifecycle attribute. This will prevent Terraform from perpetually deleting and re-allocating the address when it sees a different available IP address in the `phpipam_first_free_address` data source. +**Example:** + ``` // Look up the subnet data "phpipam_subnet" "subnet" { @@ -490,6 +599,10 @@ resource "phpipam_address" { hostname = "tf-test-host.example.internal" description = "Managed by Terraform" + custom_fields = { + CustomTestAddresses = "terraform-test" + } + lifecycle { ignore_changes = [ "subnet_id", @@ -528,6 +641,13 @@ The resource takes the following parameters: probes. * `remove_dns_on_delete` (Optional) - Removes DNS records created by PHPIPAM when the address is deleted from Terraform. Defaults to `true`. + * `custom_fields` (Optional) - A key/value map of custom fields for this address. + +⚠️ **NOTE on custom fields:** PHPIPAM installations with custom fields must have +all fields set to optional when using this plugin. For more info see +[here](https://github.com/phpipam/phpipam/issues/1073). Further to this, either +ensure that your fields also do not have default values, or ensure the default +is set in your TF configuration. Diff loops may happen otherwise! ##### Attribute Reference @@ -544,7 +664,7 @@ that subnets and IP addresses are entered into. Use this resource if you want to manage a section entirely from Terraform. If you just need to get information on a section use the `phpipam_section` data source instead. -Example: +**Example:** ``` // Create a section @@ -590,7 +710,7 @@ things, such as storing the IDs of the subnets you create for AWS, or for a full top-down management of subnets and IP addresses in Terraform. If you just need to get information on a subnet, use the `phpipam_subnet` data source instead. -Example: +**Example:** ``` data "phpipam_section" "section" { @@ -598,9 +718,13 @@ data "phpipam_section" "section" { } resource "phpipam_subnet" "subnet" { - section_id = "${data.phpipam_section.section.section_id}" + section_id = "${data.phpipam_section.section.section_id}" subnet_address = "10.10.3.0" - subnet_mask = 24 + subnet_mask = 24 + + custom_fields = { + CustomTestSubnets = "terraform-test" + } } ``` @@ -645,6 +769,14 @@ The resource takes the following parameters: `Used`). * `utilization_threshold` (Optional) - The subnet's utilization threshold. * `location_id` (Optional) - The ID of the location for this subnet. + * `custom_fields` (Optional) - A key/value map of custom fields for this + subnet. + +⚠️ **NOTE on custom fields:** PHPIPAM installations with custom fields must have +all fields set to optional when using this plugin. For more info see +[here](https://github.com/phpipam/phpipam/issues/1073). Further to this, either +ensure that your fields also do not have default values, or ensure the default +is set in your TF configuration. Diff loops may happen otherwise! ##### Attribute Reference @@ -662,13 +794,17 @@ set up a VLAN through Terraform, or update details such as its name or description. If you are just looking for information on a VLAN, use the `phpipam_vlan` data source instead. -Example: +**Example:** ``` resource "phpipam_vlan" "vlan" { name = "tf-test" number = 1000 description = "Managed by Terraform" + + custom_fields = { + CustomTestVLANs = "terraform-test" + } } ``` @@ -681,6 +817,14 @@ The resource takes the following parameters: * `l2_domain_id` (Optional) - The layer 2 domain ID in the PHPIPAM database. * `description` (Optional) - The description supplied to the VLAN. * `edit_date` (Optional) - The date this resource was last updated. + * `custom_fields` (Optional) - A key/value map of custom fields for this + VLAN. + +⚠️ **NOTE on custom fields:** PHPIPAM installations with custom fields must have +all fields set to optional when using this plugin. For more info see +[here](https://github.com/phpipam/phpipam/issues/1073). Further to this, either +ensure that your fields also do not have default values, or ensure the default +is set in your TF configuration. Diff loops may happen otherwise! ##### Attribute Reference diff --git a/plugin/providers/phpipam/address_structure.go b/plugin/providers/phpipam/address_structure.go index e9fe137..dbd6e95 100644 --- a/plugin/providers/phpipam/address_structure.go +++ b/plugin/providers/phpipam/address_structure.go @@ -1,6 +1,7 @@ package phpipam import ( + "regexp" "strconv" "github.com/hashicorp/terraform/helper/schema" @@ -85,6 +86,9 @@ func bareAddressSchema() map[string]*schema.Schema { "edit_date": &schema.Schema{ Type: schema.TypeString, }, + "custom_fields": &schema.Schema{ + Type: schema.TypeMap, + }, } } @@ -100,6 +104,8 @@ func resourceAddressSchema() map[string]*schema.Schema { case k == "subnet_id" || k == "ip_address": v.Required = true v.ForceNew = true + case k == "custom_fields": + v.Optional = true case resourceAddressOptionalFields.Has(k): v.Optional = true v.Computed = true @@ -151,6 +157,27 @@ func dataSourceAddressSchema() map[string]*schema.Schema { v.Computed = true } } + + // Add the custom_field_filter_key and custom_field_filter_value item to the + // schema. These are meta-parameters that allows searching for a custom field + // value in the data source. + s["custom_field_filter_key"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ip_address", "address_id", "hostname", "description"}, + } + s["custom_field_filter_value"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ip_address", "address_id", "hostname", "description"}, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + _, err := regexp.Compile(v.(string)) + if err != nil { + errors = append(errors, err) + } + return + }, + } return s } @@ -175,7 +202,6 @@ func expandAddress(d *schema.ResourceData) addresses.Address { Note: d.Get("note").(string), LastSeen: d.Get("last_seen").(string), ExcludePing: phpipam.BoolIntString(d.Get("exclude_ping").(bool)), - EditDate: d.Get("edit_date").(string), } return s diff --git a/plugin/providers/phpipam/custom_field_structure.go b/plugin/providers/phpipam/custom_field_structure.go new file mode 100644 index 0000000..7fab07c --- /dev/null +++ b/plugin/providers/phpipam/custom_field_structure.go @@ -0,0 +1,101 @@ +package phpipam + +import ( + "fmt" + "reflect" + "regexp" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/paybyphone/phpipam-sdk-go/controllers/addresses" + "github.com/paybyphone/phpipam-sdk-go/controllers/subnets" + "github.com/paybyphone/phpipam-sdk-go/controllers/vlans" +) + +// customFieldFilter takes a map[string]interface{} and attempts to find a +// match based off the key, and value attributes. The data is matched against +// value as a regex. For exact matching, ensure your match is enclosed in the ^ +// (start of line) and the $ (end of line) anchors. +// +// PHPIPAM currently stringifies most, if not all, values coming out of the +// API. As such, we don't attempt to cast here - anything that is not a string +// is an error. If the need arises for this to be changed at some point in time +// this function will be updated. +func customFieldFilter(data map[string]interface{}, matchKey, matchValue string) (bool, error) { + for k, w := range data { + if k == matchKey { + switch v := w.(type) { + case string: + return regexp.MatchString(matchValue, v) + default: + return false, fmt.Errorf("Key %s's value is not a string or stringified value, which we currently do not support (%#v)", k, v) + } + } + } + return false, nil +} + +// trimMap goes thru a map[string]interface{}, and removes keys that +// have zero or nil values. +func trimMap(in map[string]interface{}) { + for k, v := range in { + switch { + case v == nil: + fallthrough + case reflect.ValueOf(v).Interface() == reflect.Zero(reflect.TypeOf(v)).Interface(): + delete(in, k) + } + } +} + +// updateCustomFields performs an update of custom fields on a resource, with +// the following stipulations: +// * If we have custom fields, we need to do a diff on what is set versus +// what isn't set, and ensure that we clear out the keys that aren't set. +// Since our SDK does not currently support NOT NULL custom fields in +// PHPIPAM, we can safely set these to nil. +// * If we don't have a value for +// custom_fields at all, set all keys to nil and update so that all custom +// fields get blown away. +func updateCustomFields(d *schema.ResourceData, client interface{}) error { + customFields := make(map[string]interface{}) + if m, ok := d.GetOk("custom_fields"); ok { + customFields = m.(map[string]interface{}) + } + var old map[string]interface{} + var err error + switch c := client.(type) { + case *addresses.Controller: + old, err = c.GetAddressCustomFields(d.Get("address_id").(int)) + case *subnets.Controller: + old, err = c.GetSubnetCustomFields(d.Get("subnet_id").(int)) + case *vlans.Controller: + old, err = c.GetVLANCustomFields(d.Get("vlan_id").(int)) + default: + panic(fmt.Errorf("Invalid client type passed %#v - this is a bug!!!", client)) + } + if err != nil { + return fmt.Errorf("Error getting custom fields for updating: %s", err) + } +nextKey: + for k := range old { + for l, v := range customFields { + if k == l { + customFields[l] = v + continue nextKey + } + } + customFields[k] = nil + } + + switch c := client.(type) { + case *addresses.Controller: + _, err = c.UpdateAddressCustomFields(d.Get("address_id").(int), customFields) + case *subnets.Controller: + _, err = c.UpdateSubnetCustomFields(d.Get("subnet_id").(int), customFields) + case *vlans.Controller: + _, err = c.UpdateVLANCustomFields(d.Get("vlan_id").(int), d.Get("name").(string), customFields) + default: + panic(fmt.Errorf("Invalid client type passed %#v - this is a bug!!!", client)) + } + return err +} diff --git a/plugin/providers/phpipam/data_source_phpipam_address.go b/plugin/providers/phpipam/data_source_phpipam_address.go index 9e9cec6..6f91067 100644 --- a/plugin/providers/phpipam/data_source_phpipam_address.go +++ b/plugin/providers/phpipam/data_source_phpipam_address.go @@ -38,8 +38,8 @@ func dataSourcePHPIPAMAddressRead(d *schema.ResourceData, meta interface{}) erro return errors.New("Address search returned either zero or multiple results. Please correct your search and try again") } out = v[0] - case d.Get("subnet_id").(int) != 0 && (d.Get("description").(string) != "" || d.Get("hostname").(string) != ""): - // If subnet_id and one of description or hostname were defined, we do a + case d.Get("subnet_id").(int) != 0 && (d.Get("description").(string) != "" || d.Get("hostname").(string) != "" || d.Get("custom_field_filter_key").(string) != ""): + // If subnet_id and one of description or hostname were defined, we do // search via GetAddressesInSubnet and return the first found for one of // the fields. v, err := s.GetAddressesInSubnet(d.Get("subnet_id").(int)) @@ -58,15 +58,37 @@ func dataSourcePHPIPAMAddressRead(d *schema.ResourceData, meta interface{}) erro result = n case d.Get("hostname").(string) != "" && r.Hostname == d.Get("hostname").(string): result = n + case d.Get("custom_field_filter_key").(string) != "": + fields, err := c.GetAddressCustomFields(r.ID) + if err != nil { + return err + } + matchKey := d.Get("custom_field_filter_key").(string) + matchValue := d.Get("custom_field_filter_value").(string) + matched, err := customFieldFilter(fields, matchKey, matchValue) + if err != nil { + return err + } + if matched { + result = n + } } } if result == -1 { - return fmt.Errorf("No address found in subnet id %d with supplied description or hostname", d.Get("subnet_id")) + return fmt.Errorf("No address found in subnet id %d with supplied description, hostname or custom field value", d.Get("subnet_id")) } out = v[result] default: - return errors.New("No valid combination of parameters found - need one of address_id, ip_address, or subnet_id and (description|hostname)") + return errors.New("No valid combination of parameters found - need one of address_id, ip_address, or subnet_id and (description|hostname|custom_field_filter_key)") } flattenAddress(out, d) + fields, err := c.GetAddressCustomFields(out.ID) + if err != nil { + return err + } + trimMap(fields) + if err := d.Set("custom_fields", fields); err != nil { + return err + } return nil } diff --git a/plugin/providers/phpipam/data_source_phpipam_address_test.go b/plugin/providers/phpipam/data_source_phpipam_address_test.go index 91a6d74..b81cf5e 100644 --- a/plugin/providers/phpipam/data_source_phpipam_address_test.go +++ b/plugin/providers/phpipam/data_source_phpipam_address_test.go @@ -26,6 +26,25 @@ data "phpipam_address" "address_by_description" { } ` +const testAccDataSourcePHPIPAMAddressCustomFieldConfig = ` +resource "phpipam_address" "address" { + subnet_id = 3 + ip_address = "10.10.1.10" + description = "Terraform test address (custom fields)" + hostname = "tf-test.cust1.local" + + custom_fields = { + CustomTestAddresses = "terraform-test" + } +} + +data "phpipam_address" "custom_search" { + subnet_id = "${phpipam_address.address.subnet_id}" + custom_field_filter_key = "CustomTestAddresses" + custom_field_filter_value = ".*terraform.*" +} +` + func TestAccDataSourcePHPIPAMAddress(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -44,3 +63,22 @@ func TestAccDataSourcePHPIPAMAddress(t *testing.T) { }, }) } + +func TestAccDataSourcePHPIPAMAddress_CustomField(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDataSourcePHPIPAMAddressCustomFieldConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.phpipam_address.custom_search", "subnet_id", "3"), + resource.TestCheckResourceAttr("data.phpipam_address.custom_search", "ip_address", "10.10.1.10"), + resource.TestCheckResourceAttr("data.phpipam_address.custom_search", "description", "Terraform test address (custom fields)"), + resource.TestCheckResourceAttr("data.phpipam_address.custom_search", "hostname", "tf-test.cust1.local"), + resource.TestCheckResourceAttr("data.phpipam_address.custom_search", "custom_fields.CustomTestAddresses", "terraform-test"), + ), + }, + }, + }) +} diff --git a/plugin/providers/phpipam/data_source_phpipam_subnet.go b/plugin/providers/phpipam/data_source_phpipam_subnet.go index e91623e..a94b28e 100644 --- a/plugin/providers/phpipam/data_source_phpipam_subnet.go +++ b/plugin/providers/phpipam/data_source_phpipam_subnet.go @@ -42,7 +42,7 @@ func dataSourcePHPIPAMSubnetRead(d *schema.ResourceData, meta interface{}) error return errors.New("CIDR search returned either zero or multiple results. Please correct your search and try again") } out = v[0] - case d.Get("section_id").(int) != 0 && (d.Get("description").(string) != "" || d.Get("description_match").(string) != ""): + case d.Get("section_id").(int) != 0 && (d.Get("description").(string) != "" || d.Get("description_match").(string) != "" || d.Get("custom_field_filter_key").(string) != ""): // If section_id and description were both defined, we do a search via // GetSubnetsInSection for the description and return the first match. v, err := s.GetSubnetsInSection(d.Get("section_id").(int)) @@ -64,15 +64,41 @@ func dataSourcePHPIPAMSubnetRead(d *schema.ResourceData, meta interface{}) error } case d.Get("description").(string) != "" && r.Description == d.Get("description").(string): result = n + case d.Get("custom_field_filter_key").(string) != "": + // Skip folders for now as there is issues pulling them down in the API. + if r.IsFolder { + continue + } + fields, err := c.GetSubnetCustomFields(r.ID) + if err != nil { + return err + } + matchKey := d.Get("custom_field_filter_key").(string) + matchValue := d.Get("custom_field_filter_value").(string) + matched, err := customFieldFilter(fields, matchKey, matchValue) + if err != nil { + return err + } + if matched { + result = n + } } } if result == -1 { - return fmt.Errorf("No subnet found in section id %d with description %s", d.Get("section_id"), d.Get("description")) + return fmt.Errorf("No subnet found in section id %d with supplied description for description/custom field filter", d.Get("section_id")) } out = v[result] default: - return errors.New("No valid combination of parameters found - need one of subnet_id, subnet_address and subnet_mask, or section_id and (description|description_match)") + return errors.New("No valid combination of parameters found - need one of subnet_id, subnet_address and subnet_mask, or section_id and (description|description_match|custom_field_filter_key)") } flattenSubnet(out, d) + fields, err := c.GetSubnetCustomFields(out.ID) + if err != nil { + return err + } + trimMap(fields) + if err := d.Set("custom_fields", fields); err != nil { + return err + } return nil } diff --git a/plugin/providers/phpipam/data_source_phpipam_subnet_test.go b/plugin/providers/phpipam/data_source_phpipam_subnet_test.go index 46cdad8..6c58599 100644 --- a/plugin/providers/phpipam/data_source_phpipam_subnet_test.go +++ b/plugin/providers/phpipam/data_source_phpipam_subnet_test.go @@ -27,6 +27,25 @@ data "phpipam_subnet" "subnet_by_description_match" { } ` +const testAccDataSourcePHPIPAMSubnetCustomFieldConfig = ` +resource "phpipam_subnet" "subnet" { + subnet_address = "10.10.3.0" + subnet_mask = 24 + description = "Terraform test subnet (custom fields)" + section_id = 1 + + custom_fields = { + CustomTestSubnets = "terraform-test" + } +} + +data "phpipam_subnet" "custom_search" { + section_id = "${phpipam_subnet.subnet.section_id}" + custom_field_filter_key = "CustomTestSubnets" + custom_field_filter_value = ".*terraform.*" +} +` + func TestAccDataSourcePHPIPAMSubnet(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -46,3 +65,21 @@ func TestAccDataSourcePHPIPAMSubnet(t *testing.T) { }, }) } + +func TestAccDataSourcePHPIPAMSubnet_CustomFields(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDataSourcePHPIPAMSubnetCustomFieldConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.phpipam_subnet.custom_search", "subnet_address", "10.10.3.0"), + resource.TestCheckResourceAttr("data.phpipam_subnet.custom_search", "subnet_mask", "24"), + resource.TestCheckResourceAttr("data.phpipam_subnet.custom_search", "description", "Terraform test subnet (custom fields)"), + resource.TestCheckResourceAttr("data.phpipam_subnet.custom_search", "custom_fields.CustomTestSubnets", "terraform-test"), + ), + }, + }, + }) +} diff --git a/plugin/providers/phpipam/resource_phpipam_address.go b/plugin/providers/phpipam/resource_phpipam_address.go index a6f9204..dd87520 100644 --- a/plugin/providers/phpipam/resource_phpipam_address.go +++ b/plugin/providers/phpipam/resource_phpipam_address.go @@ -1,6 +1,9 @@ package phpipam import ( + "errors" + "fmt" + "github.com/hashicorp/terraform/helper/schema" "github.com/paybyphone/phpipam-sdk-go/phpipam" ) @@ -31,6 +34,23 @@ func resourcePHPIPAMAddressCreate(d *schema.ResourceData, meta interface{}) erro return err } + // If we have custom fields, set them now. We need to get the IP address's ID + // beforehand. + if customFields, ok := d.GetOk("custom_fields"); ok { + addrs, err := c.GetAddressesByIP(in.IPAddress) + if err != nil { + return fmt.Errorf("Could not read IP address after creating: %s", err) + } + + if len(addrs) != 1 { + return errors.New("IP address either missing or multiple results returned by reading IP after creation") + } + + if _, err := c.UpdateAddressCustomFields(addrs[0].ID, customFields.(map[string]interface{})); err != nil { + return err + } + } + return dataSourcePHPIPAMAddressRead(d, meta) } @@ -45,6 +65,10 @@ func resourcePHPIPAMAddressUpdate(d *schema.ResourceData, meta interface{}) erro return err } + if err := updateCustomFields(d, c); err != nil { + return err + } + return dataSourcePHPIPAMAddressRead(d, meta) } diff --git a/plugin/providers/phpipam/resource_phpipam_address_test.go b/plugin/providers/phpipam/resource_phpipam_address_test.go index c3dc537..a03b0bc 100644 --- a/plugin/providers/phpipam/resource_phpipam_address_test.go +++ b/plugin/providers/phpipam/resource_phpipam_address_test.go @@ -21,6 +21,28 @@ resource "phpipam_address" "address" { } ` +const testAccResourcePHPIPAMAddressCustomFieldConfig = ` +resource "phpipam_address" "address" { + subnet_id = 3 + ip_address = "10.10.1.10" + description = "Terraform test address (custom fields)" + hostname = "tf-test.cust1.local" + + custom_fields = { + CustomTestAddresses = "terraform-test" + } +} +` + +const testAccResourcePHPIPAMAddressCustomFieldUpdateConfig = ` +resource "phpipam_address" "address" { + subnet_id = 3 + ip_address = "10.10.1.10" + description = "Terraform test address (custom fields), step 2" + hostname = "tf-test.cust1.local" +} +` + func TestAccResourcePHPIPAMAddress(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -41,6 +63,38 @@ func TestAccResourcePHPIPAMAddress(t *testing.T) { }) } +func TestAccResourcePHPIPAMAddress_CustomFields(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckResourcePHPIPAMAddressDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccResourcePHPIPAMAddressCustomFieldConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourcePHPIPAMAddressCreated, + resource.TestCheckResourceAttr("phpipam_address.address", "subnet_id", "3"), + resource.TestCheckResourceAttr("phpipam_address.address", "ip_address", "10.10.1.10"), + resource.TestCheckResourceAttr("phpipam_address.address", "description", "Terraform test address (custom fields)"), + resource.TestCheckResourceAttr("phpipam_address.address", "hostname", "tf-test.cust1.local"), + resource.TestCheckResourceAttr("phpipam_address.address", "custom_fields.CustomTestAddresses", "terraform-test"), + ), + }, + resource.TestStep{ + Config: testAccResourcePHPIPAMAddressCustomFieldUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourcePHPIPAMAddressCreated, + resource.TestCheckResourceAttr("phpipam_address.address", "subnet_id", "3"), + resource.TestCheckResourceAttr("phpipam_address.address", "ip_address", "10.10.1.10"), + resource.TestCheckResourceAttr("phpipam_address.address", "description", "Terraform test address (custom fields), step 2"), + resource.TestCheckResourceAttr("phpipam_address.address", "hostname", "tf-test.cust1.local"), + resource.TestCheckNoResourceAttr("phpipam_address.address", "custom_fields.CustomTestAddresses"), + ), + }, + }, + }) +} + func testAccCheckResourcePHPIPAMAddressCreated(s *terraform.State) error { r, ok := s.RootModule().Resources[testAccResourcePHPIPAMAddressName] if !ok { diff --git a/plugin/providers/phpipam/resource_phpipam_subnet.go b/plugin/providers/phpipam/resource_phpipam_subnet.go index bd429bf..eb50f93 100644 --- a/plugin/providers/phpipam/resource_phpipam_subnet.go +++ b/plugin/providers/phpipam/resource_phpipam_subnet.go @@ -1,6 +1,11 @@ package phpipam -import "github.com/hashicorp/terraform/helper/schema" +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform/helper/schema" +) // resourcePHPIPAMSubnet returns the resource structure for the phpipam_subnet // resource. @@ -28,6 +33,23 @@ func resourcePHPIPAMSubnetCreate(d *schema.ResourceData, meta interface{}) error return err } + // If we have custom fields, set them now. We need to get the subnet's ID + // beforehand. + if customFields, ok := d.GetOk("custom_fields"); ok { + subnets, err := c.GetSubnetsByCIDR(fmt.Sprintf("%s/%d", in.SubnetAddress, in.Mask)) + if err != nil { + return fmt.Errorf("Could not read subnet after creating: %s", err) + } + + if len(subnets) != 1 { + return errors.New("Subnet either missing or multiple results returned by reading subnet after creation") + } + + if _, err := c.UpdateSubnetCustomFields(subnets[0].ID, customFields.(map[string]interface{})); err != nil { + return err + } + } + return dataSourcePHPIPAMSubnetRead(d, meta) } @@ -44,6 +66,10 @@ func resourcePHPIPAMSubnetUpdate(d *schema.ResourceData, meta interface{}) error return err } + if err := updateCustomFields(d, c); err != nil { + return err + } + return dataSourcePHPIPAMSubnetRead(d, meta) } diff --git a/plugin/providers/phpipam/resource_phpipam_subnet_test.go b/plugin/providers/phpipam/resource_phpipam_subnet_test.go index e6a5ab2..b290995 100644 --- a/plugin/providers/phpipam/resource_phpipam_subnet_test.go +++ b/plugin/providers/phpipam/resource_phpipam_subnet_test.go @@ -21,6 +21,29 @@ resource "phpipam_subnet" "subnet" { } ` +const testAccResourcePHPIPAMSubnetCustomFieldConfig = ` +resource "phpipam_subnet" "subnet" { + subnet_address = "10.10.3.0" + subnet_mask = 24 + description = "Terraform test subnet (custom fields)" + section_id = 1 + + custom_fields = { + CustomTestSubnets = "terraform-test" + } +} +` + +const testAccResourcePHPIPAMSubnetCustomFieldUpdateConfig = ` +resource "phpipam_subnet" "subnet" { + subnet_address = "10.10.3.0" + subnet_mask = 24 + description = "Terraform test subnet (custom fields), step 2" + section_id = 1 +} + +` + func TestAccResourcePHPIPAMSubnet(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -40,6 +63,36 @@ func TestAccResourcePHPIPAMSubnet(t *testing.T) { }) } +func TestAccResourcePHPIPAMSubnet_CustomFields(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckResourcePHPIPAMSubnetDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccResourcePHPIPAMSubnetCustomFieldConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourcePHPIPAMSubnetCreated, + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "subnet_address", "10.10.3.0"), + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "subnet_mask", "24"), + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "description", "Terraform test subnet (custom fields)"), + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "custom_fields.CustomTestSubnets", "terraform-test"), + ), + }, + resource.TestStep{ + Config: testAccResourcePHPIPAMSubnetCustomFieldUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourcePHPIPAMSubnetCreated, + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "subnet_address", "10.10.3.0"), + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "subnet_mask", "24"), + resource.TestCheckResourceAttr("phpipam_subnet.subnet", "description", "Terraform test subnet (custom fields), step 2"), + resource.TestCheckNoResourceAttr("phpipam_subnet.subnet", "custom_fields.CustomTestSubnets"), + ), + }, + }, + }) +} + func testAccCheckResourcePHPIPAMSubnetCreated(s *terraform.State) error { r, ok := s.RootModule().Resources[testAccResourcePHPIPAMSubnetName] if !ok { diff --git a/plugin/providers/phpipam/resource_phpipam_vlan.go b/plugin/providers/phpipam/resource_phpipam_vlan.go index 9da2b65..71a0685 100644 --- a/plugin/providers/phpipam/resource_phpipam_vlan.go +++ b/plugin/providers/phpipam/resource_phpipam_vlan.go @@ -1,6 +1,11 @@ package phpipam -import "github.com/hashicorp/terraform/helper/schema" +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform/helper/schema" +) // resourcePHPIPAMVLAN returns the resource structure for the phpipam_vlan // resource. @@ -28,6 +33,23 @@ func resourcePHPIPAMVLANCreate(d *schema.ResourceData, meta interface{}) error { return err } + // If we have custom fields, set them now. We need to get the IP address's ID + // beforehand. + if customFields, ok := d.GetOk("custom_fields"); ok { + vlans, err := c.GetVLANsByNumber(in.Number) + if err != nil { + return fmt.Errorf("Could not read VLAN after creating: %s", err) + } + + if len(vlans) != 1 { + return errors.New("VLAN either missing or multiple results returned by reading VLAN after creation") + } + + if _, err := c.UpdateVLANCustomFields(vlans[0].ID, vlans[0].Name, customFields.(map[string]interface{})); err != nil { + return err + } + } + return dataSourcePHPIPAMVLANRead(d, meta) } @@ -39,6 +61,10 @@ func resourcePHPIPAMVLANUpdate(d *schema.ResourceData, meta interface{}) error { return err } + if err := updateCustomFields(d, c); err != nil { + return err + } + return dataSourcePHPIPAMVLANRead(d, meta) } diff --git a/plugin/providers/phpipam/resource_phpipam_vlan_test.go b/plugin/providers/phpipam/resource_phpipam_vlan_test.go index 7cb7193..415ff56 100644 --- a/plugin/providers/phpipam/resource_phpipam_vlan_test.go +++ b/plugin/providers/phpipam/resource_phpipam_vlan_test.go @@ -20,6 +20,26 @@ resource "phpipam_vlan" "vlan" { } ` +const testAccResourcePHPIPAMVLANCustomFieldConfig = ` +resource "phpipam_vlan" "vlan" { + name = "terraform" + number = 1999 + description = "Terraform test vlan (custom field)" + + custom_fields = { + CustomTestVLANs = "terraform-test" + } +} +` + +const testAccResourcePHPIPAMVLANCustomFieldUpdateConfig = ` +resource "phpipam_vlan" "vlan" { + name = "terraform" + number = 1999 + description = "Terraform test vlan (custom field), step 2" +} +` + func TestAccResourcePHPIPAMVLAN(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -39,6 +59,36 @@ func TestAccResourcePHPIPAMVLAN(t *testing.T) { }) } +func TestAccResourcePHPIPAMVLAN_CustomFields(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckResourcePHPIPAMVLANDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccResourcePHPIPAMVLANCustomFieldConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourcePHPIPAMVLANCreated, + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "name", "terraform"), + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "number", "1999"), + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "description", "Terraform test vlan (custom field)"), + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "custom_fields.CustomTestVLANs", "terraform-test"), + ), + }, + resource.TestStep{ + Config: testAccResourcePHPIPAMVLANCustomFieldUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourcePHPIPAMVLANCreated, + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "name", "terraform"), + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "number", "1999"), + resource.TestCheckResourceAttr("phpipam_vlan.vlan", "description", "Terraform test vlan (custom field), step 2"), + resource.TestCheckNoResourceAttr("phpipam_vlan.vlan", "custom_fields.CustomTestVLANs"), + ), + }, + }, + }) +} + func testAccCheckResourcePHPIPAMVLANCreated(s *terraform.State) error { r, ok := s.RootModule().Resources[testAccResourcePHPIPAMVLANName] if !ok { diff --git a/plugin/providers/phpipam/subnet_structure.go b/plugin/providers/phpipam/subnet_structure.go index c06c701..89e9af3 100644 --- a/plugin/providers/phpipam/subnet_structure.go +++ b/plugin/providers/phpipam/subnet_structure.go @@ -107,6 +107,9 @@ func bareSubnetSchema() map[string]*schema.Schema { "edit_date": &schema.Schema{ Type: schema.TypeString, }, + "custom_fields": &schema.Schema{ + Type: schema.TypeMap, + }, } } @@ -124,6 +127,8 @@ func resourceSubnetSchema() map[string]*schema.Schema { v.ForceNew = true case k == "section_id": v.Required = true + case k == "custom_fields": + v.Optional = true case resourceSubnetOptionalFields.Has(k): v.Optional = true v.Computed = true @@ -179,6 +184,26 @@ func dataSourceSubnetSchema() map[string]*schema.Schema { return }, } + // Add the custom_field_filter_key and custom_field_filter_value item to the + // schema. These are meta-parameters that allows searching for a custom field + // value in the data source. + s["custom_field_filter_key"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"subnet_id", "subnet_address", "subnet_mask", "description", "description_match"}, + } + s["custom_field_filter_value"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"subnet_id", "subnet_address", "subnet_mask", "description", "description_match"}, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + _, err := regexp.Compile(v.(string)) + if err != nil { + errors = append(errors, err) + } + return + }, + } return s } @@ -219,7 +244,6 @@ func expandSubnet(d *schema.ResourceData) subnets.Subnet { IsFull: phpipam.BoolIntString(d.Get("is_full").(bool)), Threshold: d.Get("utilization_threshold").(int), Location: d.Get("location_id").(int), - EditDate: d.Get("edit_date").(string), } return s diff --git a/plugin/providers/phpipam/vlan_structure.go b/plugin/providers/phpipam/vlan_structure.go index 87820f0..ddd2bff 100644 --- a/plugin/providers/phpipam/vlan_structure.go +++ b/plugin/providers/phpipam/vlan_structure.go @@ -41,6 +41,9 @@ func bareVLANSchema() map[string]*schema.Schema { "edit_date": &schema.Schema{ Type: schema.TypeString, }, + "custom_fields": &schema.Schema{ + Type: schema.TypeMap, + }, } } @@ -55,6 +58,8 @@ func resourceVLANSchema() map[string]*schema.Schema { // VLAN name and number are required case k == "name" || k == "number": v.Required = true + case k == "custom_fields": + v.Optional = true case resourceVLANOptionalFields.Has(k): v.Optional = true v.Computed = true @@ -70,8 +75,8 @@ func resourceVLANSchema() map[string]*schema.Schema { // entry ID and VLAN number. It also ensures that all fields are computed as // well. func dataSourceVLANSchema() map[string]*schema.Schema { - schema := bareVLANSchema() - for k, v := range schema { + s := bareVLANSchema() + for k, v := range s { switch k { case "vlan_id": v.Optional = true @@ -85,7 +90,7 @@ func dataSourceVLANSchema() map[string]*schema.Schema { v.Computed = true } } - return schema + return s } // expandVLAN returns the vlans.VLAN structure for a @@ -98,7 +103,6 @@ func expandVLAN(d *schema.ResourceData) vlans.VLAN { Name: d.Get("name").(string), Number: d.Get("number").(int), Description: d.Get("description").(string), - EditDate: d.Get("edit_date").(string), } return v diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/addresses/addresses.go b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/addresses/addresses.go index 94b0397..5ed6921 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/addresses/addresses.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/addresses/addresses.go @@ -99,12 +99,33 @@ func (c *Controller) GetAddressesByIP(ipaddr string) (out []Address, err error) return } +// GetAddressCustomFieldsSchema GETs the custom fields for the addresses controller via +// client.GetCustomFieldsSchema. +func (c *Controller) GetAddressCustomFieldsSchema() (out map[string]phpipam.CustomField, err error) { + out, err = c.Client.GetCustomFieldsSchema("addresses") + return +} + +// GetAddressCustomFields GETs the custom fields for a subnet via +// client.GetCustomFields. +func (c *Controller) GetAddressCustomFields(id int) (out map[string]interface{}, err error) { + out, err = c.Client.GetCustomFields(id, "addresses") + return +} + // UpdateAddress updates an address by sending a PATCH request. func (c *Controller) UpdateAddress(in Address) (message string, err error) { err = c.SendRequest("PATCH", "/addresses/", &in, &message) return } +// UpdateAddressCustomFields PATCHes the subnet's custom fields via +// client.UpdateCustomFields. +func (c *Controller) UpdateAddressCustomFields(id int, in map[string]interface{}) (message string, err error) { + message, err = c.Client.UpdateCustomFields(id, in, "addresses") + return +} + // DeleteAddress deletes an address by ID. RemoveDNS can be set to true if you // want to have any related DNS records deleted as well. func (c *Controller) DeleteAddress(id int, RemoveDNS phpipam.BoolIntString) (message string, err error) { diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go index 81eba44..9f972a3 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go @@ -141,6 +141,20 @@ func (c *Controller) GetAddressesInSubnet(id int) (out []addresses.Address, err return } +// GetSubnetCustomFieldsSchema GETs the custom fields for the subnets controller via +// client.GetCustomFieldsSchema. +func (c *Controller) GetSubnetCustomFieldsSchema() (out map[string]phpipam.CustomField, err error) { + out, err = c.Client.GetCustomFieldsSchema("subnets") + return +} + +// GetSubnetCustomFields GETs the custom fields for a subnet via +// client.GetCustomFields. +func (c *Controller) GetSubnetCustomFields(id int) (out map[string]interface{}, err error) { + out, err = c.Client.GetCustomFields(id, "subnets") + return +} + // UpdateSubnet updates a subnet by sending a PATCH request. // // Note you cannot use this function to update a subnet's CIDR - to split, @@ -151,6 +165,13 @@ func (c *Controller) UpdateSubnet(in Subnet) (message string, err error) { return } +// UpdateSubnetCustomFields PATCHes the subnet's custom fields via +// client.UpdateCustomFields. +func (c *Controller) UpdateSubnetCustomFields(id int, in map[string]interface{}) (message string, err error) { + message, err = c.Client.UpdateCustomFields(id, in, "subnets") + return +} + // DeleteSubnet deletes a subnet by its ID. func (c *Controller) DeleteSubnet(id int) (message string, err error) { err = c.SendRequest("DELETE", fmt.Sprintf("/subnets/%d/", id), &struct{}{}, &message) diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/vlans/vlans.go b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/vlans/vlans.go index 42aea20..f13669f 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/vlans/vlans.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/vlans/vlans.go @@ -5,6 +5,7 @@ package vlans import ( "fmt" + "github.com/paybyphone/phpipam-sdk-go/phpipam" "github.com/paybyphone/phpipam-sdk-go/phpipam/client" "github.com/paybyphone/phpipam-sdk-go/phpipam/session" ) @@ -67,12 +68,62 @@ func (c *Controller) GetVLANsByNumber(id int) (out []VLAN, err error) { return } +// GetVLANCustomFieldsSchema GETs the custom fields for the vlans controller via +// client.GetCustomFieldsSchema. +func (c *Controller) GetVLANCustomFieldsSchema() (out map[string]phpipam.CustomField, err error) { + out, err = c.Client.GetCustomFieldsSchema("vlans") + return +} + +// GetVLANCustomFields GETs the custom fields for a subnet via +// client.GetCustomFields. +func (c *Controller) GetVLANCustomFields(id int) (out map[string]interface{}, err error) { + out, err = c.Client.GetCustomFields(id, "vlans") + return +} + // UpdateVLAN updates a VLAN by sending a PATCH request. func (c *Controller) UpdateVLAN(in VLAN) (message string, err error) { err = c.SendRequest("PATCH", "/vlans/", &in, &message) return } +// UpdateVLANCustomFields PATCHes the vlan's custom fields. +// +// This function differs from the custom field functions available in the +// addresses and subnets controller - while those two controllers do not +// require any other data outside of the ID to update the custom fields, +// updating a VLAN requires a name as well. +func (c *Controller) UpdateVLANCustomFields(id int, name string, in map[string]interface{}) (message string, err error) { + // Verify that we are only updating fields that are custom fields. + var schema map[string]phpipam.CustomField + schema, err = c.GetVLANCustomFieldsSchema() + if err != nil { + return + } + for k := range in { + for l := range schema { + if k == l { + goto customFieldFound + } + } + // not found + return "", fmt.Errorf("Custom field %s not found in schema for controller vlans", k) + // found + customFieldFound: + } + + params := make(map[string]interface{}) + for k, v := range in { + params[k] = v + } + + params["id"] = id + params["name"] = name + err = c.SendRequest("PATCH", "/vlans/", ¶ms, &message) + return +} + // DeleteVLAN deletes a VLAN by its ID. func (c *Controller) DeleteVLAN(id int) (message string, err error) { err = c.SendRequest("DELETE", fmt.Sprintf("/vlans/%d/", id), &struct{}{}, &message) diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/client/client.go b/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/client/client.go index cda173d..b8fd830 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/client/client.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/client/client.go @@ -5,6 +5,7 @@ package client import ( "fmt" + "github.com/paybyphone/phpipam-sdk-go/phpipam" "github.com/paybyphone/phpipam-sdk-go/phpipam/request" "github.com/paybyphone/phpipam-sdk-go/phpipam/session" ) @@ -71,3 +72,105 @@ func (c *Client) SendRequest(method, uri string, in, out interface{}) error { } return err } + +// GetCustomFieldsSchema GETs the custom fields for the supplied controller +// name and returns them as a map[string]phpipam.CustomField. +// +// This function is called out to in a controller to implement this +// functionality in a specific pacakge. +func (c *Client) GetCustomFieldsSchema(controller string) (out map[string]phpipam.CustomField, err error) { + err = c.SendRequest("GET", fmt.Sprintf("/%s/custom_fields/", controller), &struct{}{}, &out) + return +} + +// GetCustomFields GETs the custom fields for a resource, and returns them +// as a map[string]interface{}. A call out to GetCustomFields is performed +// first, and then a GET is performed on the subnet resource with only the +// custom fields returned. +// +// Note that due to how PHPIPAM stringifies most output, this will, in most +// cases, mean that attribute values will be strings and will need to be +// convereted externally. This function does not explicitly lock to +// map[string]string to allow for possible cases where this is not the case, +// and to also allow for future de-stringification of the JSON. +// +// This function is called out to in a controller to implement this +// functionality in a specific pacakge. +func (c *Client) GetCustomFields(id int, controller string) (out map[string]interface{}, err error) { + var schema map[string]phpipam.CustomField + schema, err = c.GetCustomFieldsSchema(controller) + if err != nil { + return + } + + out, err = c.getCustomFieldsRequest(id, controller, schema) + return +} + +// getCustomFieldsRequest performs the actual work for GetCustomFields. This is +// separated off to make testing easier. +func (c *Client) getCustomFieldsRequest(id int, controller string, schema map[string]phpipam.CustomField) (out map[string]interface{}, err error) { + err = c.SendRequest("GET", fmt.Sprintf("/%s/%d/", controller, id), &struct{}{}, &out) + if err != nil { + return + } + for k := range out { + for l := range schema { + if k == l { + goto customFieldFound + } + } + // not found + delete(out, k) + // found + customFieldFound: + } + return +} + +// UpdateCustomFields uses PATCH on a resource controller to update a specific +// resoruce ID with the custom fields provided in the key/value map defined by +// in. +// +// Internal validation is preformed first to ensure that this field is not +// setting a custom field that is *not* defined in the schema. This is to +// prevent abuse - if this was not in place, this function could technically be +// used to update *any* field, as PHPIPAM does not maintain a separate subtype +// for custom fields. +// +// This function is called out to in a controller to implement this +// functionality in a specific pacakge. +func (c *Client) UpdateCustomFields(id int, in map[string]interface{}, controller string) (message string, err error) { + var schema map[string]phpipam.CustomField + schema, err = c.GetCustomFieldsSchema(controller) + if err != nil { + return + } + message, err = c.updateCustomFieldsRequest(id, in, controller, schema) + return +} + +// updateCustomFieldsRequest performs the actual validation and request work +// for UpdateCustomFields. This is separated off to make testing easier. +func (c *Client) updateCustomFieldsRequest(id int, in map[string]interface{}, controller string, schema map[string]phpipam.CustomField) (message string, err error) { + for k := range in { + for l := range schema { + if k == l { + goto customFieldFound + } + } + // not found + return "", fmt.Errorf("Custom field %s not found in schema for controller %s", k, controller) + // found + customFieldFound: + } + + params := make(map[string]interface{}) + for k, v := range in { + params[k] = v + } + + params["id"] = id + err = c.SendRequest("PATCH", fmt.Sprintf("/%s/", controller), ¶ms, &message) + return +} diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/phpipam.go b/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/phpipam.go index 5927860..4f6e25c 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/phpipam.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/phpipam.go @@ -147,3 +147,32 @@ func (jis *JSONIntString) UnmarshalJSON(b []byte) error { return nil } + +// CustomField represents a PHPIPAM custom field schema entry. +// +// Custom fields are currently embedded in a resource's table (such as subnets +// or IP addresses) directly. Hence, in order to know what custom fields are +// currently present for a specific resource, the /custom_fields/ method of a +// controller needs to be queried first before attempting to fetch these custom +// fields individually. +type CustomField struct { + // The name of the custom field. + Name string `json:"name"` + + // The type of custom field. This directly translates to its MySQL data type + // in the applicable resource table. + Type string `json:"type"` + + // The the description of the custom field. This shows up as a tooltip in the + // UI when working with the custom field. + Comment string `json:"Comment,omitempty"` + + // If this is true, this field is required. This translates to the NOT NULL + // attribute on the respective field's column. Should be one of YES or NO. + Null string `json:"Null,omitempty"` + + // The default entry for this custom field. Note that this is always + // stringified and will need to be parsed appropriately when you reading the + // actual custom field. + Default string `json:"Default,omitempty"` +} diff --git a/vendor/vendor.json b/vendor/vendor.json index a137e5a..2ce2f7d 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -492,52 +492,52 @@ "revisionTime": "2017-03-15T16:33:13Z" }, { - "checksumSHA1": "lOZ+LVf728WfTr1l+QxPztpA+Q0=", + "checksumSHA1": "POLdeJ+e4C9O1Gg1EQul9vJ8q7U=", "path": "github.com/paybyphone/phpipam-sdk-go/controllers/addresses", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { "checksumSHA1": "8DoCZFEF8xaZrDJEfdhWqu42hf0=", "path": "github.com/paybyphone/phpipam-sdk-go/controllers/sections", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { - "checksumSHA1": "8EDty6JyKfQ4q9RYvHezvPXQehM=", + "checksumSHA1": "vidGgBwKBnajwt70e+5ekPXMmvc=", "path": "github.com/paybyphone/phpipam-sdk-go/controllers/subnets", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { - "checksumSHA1": "VeUtF8J2w1Gnwr6FdB1+EaSMu84=", + "checksumSHA1": "COAuIHHMIdQaJlZfaAYMhn7ew74=", "path": "github.com/paybyphone/phpipam-sdk-go/controllers/vlans", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { - "checksumSHA1": "1NGDMqCaySijyiiGPnSX77h0Ruo=", + "checksumSHA1": "F++orFwGmYJkwpTiDxVI3srrWzQ=", "path": "github.com/paybyphone/phpipam-sdk-go/phpipam", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { - "checksumSHA1": "/Nu1RXXPsalF69UPOuFm5Q8fURI=", + "checksumSHA1": "4mk74Bkioqkx2+D+yeR2b/Wmwyc=", "path": "github.com/paybyphone/phpipam-sdk-go/phpipam/client", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { "checksumSHA1": "SjmycWTgtT0Rit7DDNy8Sl/SShA=", "path": "github.com/paybyphone/phpipam-sdk-go/phpipam/request", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { "checksumSHA1": "R1/J0Xef+KZNQTFfaUb4EOuUD6E=", "path": "github.com/paybyphone/phpipam-sdk-go/phpipam/session", - "revision": "489df44901db9a58f9b39f37d1d3e4e5232498f3", - "revisionTime": "2017-03-23T18:01:12Z" + "revision": "7fec9c4189656f6bb4509329dfe77767ee0b03fc", + "revisionTime": "2017-04-03T00:18:55Z" }, { "checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=",