From 7172cd06ba52c84dfa55ae385c298cf530e65ccd Mon Sep 17 00:00:00 2001 From: josi19 <16536859+josi19@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:49:23 +0100 Subject: [PATCH] feat: add discovery rules resource (#2) feat: add discovery rules resource --- docs/resources/foreman_discovery_rules.md | 53 +++ examples/discovery_rule/main.tf | 21 + foreman/api/discovery_rule.go | 214 +++++++++++ foreman/foreman_api_test.go | 13 +- foreman/provider.go | 1 + foreman/resource_foreman_discoveryrule.go | 358 ++++++++++++++++++ .../resource_foreman_discoveryrule_test.go | 221 +++++++++++ .../3.11/discovery_rules/create_response.json | 28 ++ .../discovery_rules/query_response_multi.json | 61 +++ .../query_response_single.json | 27 ++ .../discovery_rules/query_response_zero.json | 12 + .../3.11/discovery_rules/read_response.json | 28 ++ .../3.11/discovery_rules/update_response.json | 28 ++ 13 files changed, 1064 insertions(+), 1 deletion(-) create mode 100644 docs/resources/foreman_discovery_rules.md create mode 100644 examples/discovery_rule/main.tf create mode 100644 foreman/api/discovery_rule.go create mode 100644 foreman/resource_foreman_discoveryrule.go create mode 100644 foreman/resource_foreman_discoveryrule_test.go create mode 100644 foreman/testdata/3.11/discovery_rules/create_response.json create mode 100644 foreman/testdata/3.11/discovery_rules/query_response_multi.json create mode 100644 foreman/testdata/3.11/discovery_rules/query_response_single.json create mode 100644 foreman/testdata/3.11/discovery_rules/query_response_zero.json create mode 100644 foreman/testdata/3.11/discovery_rules/read_response.json create mode 100644 foreman/testdata/3.11/discovery_rules/update_response.json diff --git a/docs/resources/foreman_discovery_rules.md b/docs/resources/foreman_discovery_rules.md new file mode 100644 index 00000000..c476811e --- /dev/null +++ b/docs/resources/foreman_discovery_rules.md @@ -0,0 +1,53 @@ + +# foreman_discovery_rules + +Discovery rules in Foreman are used to automatically provision hosts based on predefined criteria. +These rules help streamline the process of adding new hosts to your infrastructure by automating the provisioning process based on specific conditions. + +## Example Usage + +```terraform +resource "foreman_discovery_rule" "example_rule_01" { + name = "Example Rule HPE servers" + search = "facts.bios_vendor = HPE" + hostgroup_ids = 3 + hostname = "<%= @host.facts['nmprimary_dhcp4_option_host_name'] %>" + max_count = 0 + priority = 100 + enabled = true + location_ids = [2] + organization_ids = [1] +} +``` + +## Argument Reference + +The following arguments are supported: + +- `name` - (Required) The name of the discovery rule. +- `search` - (Required) The search criteria used to match hosts. +- `priority` - (Required) The priority of the rule. +- `hostgroup_id` - (Optional) The ID of the host group to which the discovered host will be assigned. +- `enabled` - (Optional) A boolean value indicating whether the discovery rule is enabled. When set to `true`, the rule is active and will be evaluated. +- `order` - (Optional) An integer specifying the order in which the rule is evaluated. Lower numbers are evaluated first. +- `parameters` - (Optional) A map of key-value pairs that will be saved as discovery rule parameters. These parameters can be used to pass additional information to the rule. +- `max_count` - (Optional) The maximum number of hosts that can be discovered by this rule. A value of `0` means unlimited. +- `hostname` - (Optional) The hostname pattern to be used for the discovered hosts. +- `location_ids` - (Optional) A list of location IDs where the discovered hosts will be assigned. +- `organization_ids` - (Optional) A list of organization IDs where the discovered hosts will be assigned. + +## Attributes Reference + +The following attributes are exported: + +- `name` - The name of the discovery rule. +- `search` - The search criteria used to match hosts. +- `priority` - The priority of the rule. +- `hostgroup_id` - The ID of the host group to which the discovered host will be assigned. +- `enabled` - Whether the discovery rule is enabled. +- `order` - The order in which the rule is evaluated. +- `parameters` - A map of parameters that will be saved as discovery rule parameters. +- `max_count` - (Optional) The maximum number of hosts that can be discovered by this rule. A value of `0` means unlimited. +- `hostname` - (Optional) The hostname pattern to be used for the discovered hosts. +- `location_ids` - (Optional) A list of location IDs where the discovered hosts will be assigned. +- `organization_ids` - (Optional) A list of organization IDs where the discovered hosts will be assigned. diff --git a/examples/discovery_rule/main.tf b/examples/discovery_rule/main.tf new file mode 100644 index 00000000..dbe34011 --- /dev/null +++ b/examples/discovery_rule/main.tf @@ -0,0 +1,21 @@ +provider "foreman" { + server_hostname = "192.168.1.118" + server_protocol = "https" + + client_tls_insecure = true + + client_username = "${var.client_username}" + client_password = "${var.client_password}" +} + +resource "foreman_discovery_rule" "example_rule_01" { + name = "example-rule-01" + search = "facts.bios_vendor = HPE" + hostgroup_ids = 5 + hostname = "<%= @host.facts['nmprimary_dhcp4_option_host_name'] %>" + max_count = 0 + priority = 100 + enabled = true + location_ids = [1] + organization_ids = [1] +} diff --git a/foreman/api/discovery_rule.go b/foreman/api/discovery_rule.go new file mode 100644 index 00000000..db91129d --- /dev/null +++ b/foreman/api/discovery_rule.go @@ -0,0 +1,214 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "path" + "strconv" + + "github.com/HanseMerkur/terraform-provider-utils/log" +) + +const ( + DiscoveryRuleEndpointPrefix = "/v2/discovery_rules/" +) + +type ForemanDiscoveryRule struct { + ForemanObject + Name string `json:"name"` + Search string `json:"search,omitempty"` + HostGroupId int `json:"hostgroup_id,omitempty"` + Hostname string `json:"hostname,omitempty"` + HostsLimitMaxCount int `json:"max_count,omitempty"` + Priority int `json:"priority"` + Enabled bool `json:"enabled"` + LocationIds []int `json:"location_ids,omitempty"` + OrganizationIds []int `json:"organization_ids,omitempty"` + DefaultLocationId int `json:"location_id,omitempty"` + DefaultOrganizationId int `json:"organization_id,omitempty"` +} + +type ForemanDiscoveryRuleResponse struct { + ForemanObject + Name string `json:"name"` + Search string `json:"search,omitempty"` + HostGroupId int `json:"hostgroup_id,omitempty"` + Hostname string `json:"hostname,omitempty"` + Priority int `json:"priority"` + Enabled bool `json:"enabled"` + HostsLimitMaxCount int `json:"hosts_limit,omitempty"` + Locations []EntityResponse `json:"locations,omitempty"` + Organizations []EntityResponse `json:"organizations,omitempty"` +} + +type EntityResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Title string `json:"title"` + Description any `json:"description"` +} + +// CreateDiscoveryRule creates a new ForemanDiscoveryRule +func (c *Client) CreateDiscoveryRule(ctx context.Context, d *ForemanDiscoveryRule) (*ForemanDiscoveryRule, error) { + log.Tracef("foreman/api/discovery_rule.go#Create") + + if d.DefaultLocationId == 0 { + d.DefaultLocationId = c.clientConfig.LocationID + } + + if d.DefaultOrganizationId == 0 { + d.DefaultOrganizationId = c.clientConfig.OrganizationID + } + + dJSONBytes, err := c.WrapJSON("discovery_rule", d) + if err != nil { + return nil, err + } + + log.Debugf("discoveryruleJSONBytes: [%s]", dJSONBytes) + + req, err := c.NewRequestWithContext( + ctx, + http.MethodPost, + DiscoveryRuleEndpointPrefix, + bytes.NewBuffer(dJSONBytes), + ) + if err != nil { + return nil, err + } + + var createdDiscoveryRule ForemanDiscoveryRule + if err := c.SendAndParse(req, &createdDiscoveryRule); err != nil { + return nil, err + } + + log.Debugf("createdDiscoveryRule: [%+v]", createdDiscoveryRule) + + return &createdDiscoveryRule, nil +} + +// ReadDiscoveryRule reads the ForemanDiscoveryRule identified by the supplied ID +func (c *Client) ReadDiscoveryRule(ctx context.Context, id int) (*ForemanDiscoveryRuleResponse, error) { + log.Tracef("foreman/api/discovery_rule.go#Read") + + reqEndpoint := path.Join(DiscoveryRuleEndpointPrefix, strconv.Itoa(id)) + req, err := c.NewRequestWithContext( + ctx, + http.MethodGet, + reqEndpoint, + nil, + ) + if err != nil { + return nil, err + } + + var readDiscoveryRule ForemanDiscoveryRuleResponse + if err := c.SendAndParse(req, &readDiscoveryRule); err != nil { + return nil, err + } + + log.Debugf("readDiscoveryRule: [%+v]", readDiscoveryRule) + + return &readDiscoveryRule, nil +} + +// UpdateDiscoveryRule updates the ForemanDiscoveryRule identified by the supplied ForemanDiscoveryRule +func (c *Client) UpdateDiscoveryRule(ctx context.Context, d *ForemanDiscoveryRule) (*ForemanDiscoveryRule, error) { + log.Tracef("foreman/api/discovery_rule.go#Update") + + reqEndpoint := path.Join(DiscoveryRuleEndpointPrefix, strconv.Itoa(d.Id)) + + discoveryruleJSONBytes, err := c.WrapJSON("discovery_rule", d) + if err != nil { + return nil, err + } + + log.Debugf("discoveryruleJSONBytes: [%s]", discoveryruleJSONBytes) + + req, err := c.NewRequestWithContext( + ctx, + http.MethodPut, + reqEndpoint, + bytes.NewBuffer(discoveryruleJSONBytes), + ) + if err != nil { + return nil, err + } + + var updatedDiscoveryRule ForemanDiscoveryRule + + if err := c.SendAndParse(req, &updatedDiscoveryRule); err != nil { + return nil, err + } + + log.Debugf("updatedDiscoveryRule: [%+v]", updatedDiscoveryRule) + + return &updatedDiscoveryRule, nil +} + +// DeleteDiscoveryRule deletes the ForemanDiscoveryRule identified by the supplied ID +func (c *Client) DeleteDiscoveryRule(ctx context.Context, id int) error { + log.Tracef("foreman/api/discovery_rule.go#Delete") + + reqEndpoint := path.Join(DiscoveryRuleEndpointPrefix, strconv.Itoa(id)) + + req, err := c.NewRequestWithContext( + ctx, + http.MethodDelete, + reqEndpoint, + nil, + ) + if err != nil { + return err + } + + return c.SendAndParse(req, nil) +} + +// QueryDiscoveryRule queries the ForemanDiscoveryRule identified by the supplied ForemanDiscoveryRule +func (c *Client) QueryDiscoveryRule(ctx context.Context, d *ForemanDiscoveryRule) (QueryResponse, error) { + log.Tracef("foreman/api/discovery_rule.go#Search") + + queryResponse := QueryResponse{} + + req, err := c.NewRequestWithContext( + ctx, + http.MethodGet, + DiscoveryRuleEndpointPrefix, + nil, + ) + if err != nil { + return queryResponse, err + } + + reqQuery := req.URL.Query() + reqQuery.Set("search", fmt.Sprintf("name=\"%s\"", d.Name)) + + req.URL.RawQuery = reqQuery.Encode() + if err := c.SendAndParse(req, &queryResponse); err != nil { + return queryResponse, err + } + + log.Debugf("queryResponse: [%+v]", queryResponse) + + results := []ForemanDiscoveryRule{} + resultsBytes, err := json.Marshal(queryResponse.Results) + if err != nil { + return queryResponse, err + } + + if err := json.Unmarshal(resultsBytes, &results); err != nil { + return queryResponse, err + } + + iArr := make([]interface{}, len(results)) + for idx, val := range results { + iArr[idx] = val + } + queryResponse.Results = iArr + + return queryResponse, nil +} diff --git a/foreman/foreman_api_test.go b/foreman/foreman_api_test.go index 2b545677..5bd38134 100644 --- a/foreman/foreman_api_test.go +++ b/foreman/foreman_api_test.go @@ -3,7 +3,6 @@ package foreman import ( "context" "encoding/json" - "github.com/HanseMerkur/terraform-provider-utils/log" "io" "math/rand" "net/http" @@ -14,6 +13,8 @@ import ( "strings" "testing" + "github.com/HanseMerkur/terraform-provider-utils/log" + tfrand "github.com/HanseMerkur/terraform-provider-utils/rand" "github.com/terraform-coop/terraform-provider-foreman/foreman/api" @@ -256,6 +257,8 @@ func TestCRUDFunction_CorrectURLAndMethod(t *testing.T) { testCases = append(testCases, DataSourceForemanTemplateKindCorrectURLAndMethodTestCases(t)...) + testCases = append(testCases, ResourceForemanDiscoveryRuleCorrectURLAndMethodTestCases(t)...) + cred := api.ClientCredentials{} conf := api.ClientConfig{} @@ -352,6 +355,8 @@ func TestCRUDFunction_RequestDataEmpty(t *testing.T) { testCases = append(testCases, DataSourceForemanTemplateKindRequestDataEmptyTestCases(t)...) + testCases = append(testCases, ResourceForemanDiscoveryRuleRequestDataEmptyTestCases(t)...) + cred := api.ClientCredentials{} conf := api.ClientConfig{} @@ -503,6 +508,8 @@ func TestCRUDFunction_StatusCodeError(t *testing.T) { testCases = append(testCases, DataSourceForemanTemplateKindStatusCodeTestCases(t)...) + testCases = append(testCases, ResourceForemanDiscoveryRuleStatusCodeTestCases(t)...) + cred := api.ClientCredentials{} conf := api.ClientConfig{} @@ -587,6 +594,8 @@ func TestCRUDFunction_EmptyResponseError(t *testing.T) { testCases = append(testCases, DataSourceForemanTemplateKindEmptyResponseTestCases(t)...) + testCases = append(testCases, ResourceForemanDiscoveryRuleEmptyResponseTestCases(t)...) + cred := api.ClientCredentials{} conf := api.ClientConfig{} @@ -708,6 +717,8 @@ func TestCRUDFunction_MockResponse(t *testing.T) { testCases = append(testCases, DataSourceForemanTemplateKindMockResponseTestCases(t)...) + testCases = append(testCases, ResourceForemanDiscoveryRuleMockResponseTestCases(t)...) + cred := api.ClientCredentials{} conf := api.ClientConfig{} diff --git a/foreman/provider.go b/foreman/provider.go index 33bb67f4..bb60b6e7 100644 --- a/foreman/provider.go +++ b/foreman/provider.go @@ -179,6 +179,7 @@ func Provider() *schema.Provider { "foreman_architecture": resourceForemanArchitecture(), "foreman_host": resourceForemanHost(), "foreman_hostgroup": resourceForemanHostgroup(), + "foreman_discovery_rule": resourceForemanDiscoveryRule(), "foreman_media": resourceForemanMedia(), "foreman_model": resourceForemanModel(), "foreman_operatingsystem": resourceForemanOperatingSystem(), diff --git a/foreman/resource_foreman_discoveryrule.go b/foreman/resource_foreman_discoveryrule.go new file mode 100644 index 00000000..069c1d10 --- /dev/null +++ b/foreman/resource_foreman_discoveryrule.go @@ -0,0 +1,358 @@ +package foreman + +import ( + "context" + "fmt" + "strconv" + + "github.com/HanseMerkur/terraform-provider-utils/autodoc" + "github.com/HanseMerkur/terraform-provider-utils/conv" + "github.com/HanseMerkur/terraform-provider-utils/log" + "github.com/terraform-coop/terraform-provider-foreman/foreman/api" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceForemanDiscoveryRule() *schema.Resource { + return &schema.Resource{ + + CreateContext: resourceForemanDiscoveryRuleCreate, + ReadContext: resourceForemanDiscoveryRuleRead, + UpdateContext: resourceForemanDiscoveryRuleUpdate, + DeleteContext: resourceForemanDiscoveryRuleDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + + autodoc.MetaAttribute: { + Type: schema.TypeBool, + Computed: true, + Description: "Discovery Rules are configurations within the Foreman tool that automate " + + "the provisioning of newly discovered hosts on your network." + + "They specify criteria—like hardware characteristics or network details." + + "When matched by a discovered host, trigger automatic actions such as assigning " + + "it to a host group or initiating a specific installation process." + + "This streamlines adding new servers by reducing manual setup." + + autodoc.MetaSummary, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(8, 256), + Description: fmt.Sprintf( + "Discovery Rule name. "+ + "%s \"compute\"", + autodoc.MetaExample, + ), + }, + + "search": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(8, 256), + Description: "Search query that matches specific hosts", + }, + + "hostgroup_ids": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Assing target hostgroup by ID ", + }, + + "hostname": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(8, 256), + Description: "Specifies the name of the new host. Can be a string or extracted via facts.", + }, + + "max_count": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Sets the host limit, which defines, how many host can be provisioned wiht this rule. (0 = unlimited)", + }, + + "priority": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntAtLeast(0), + Description: "Rule priority (lower integer means higher priority).", + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Enables or disables the discovery rule.", + }, + + "location_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Optional: true, + Description: "List of all locations the discovery rule can be used.", + }, + + "organization_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Optional: true, + Description: "List of all organizations the discovery rule can be used.", + }, + }, + } +} + +// buildForemanDiscoveryRule constructs a ForemanDiscoveryRule struct from a resource +// data reference. The struct's members are populated from the data populated +// in the resource data. Missing members will be left to the zero value for +// that member's type. +func buildForemanDiscoveryRule(d *schema.ResourceData) *api.ForemanDiscoveryRule { + log.Tracef("resource_foreman_discovery_rule.go#buildForemanDiscoveryRule") + + discovery_rule := api.ForemanDiscoveryRule{} + + obj := buildForemanObject(d) + discovery_rule.ForemanObject = *obj + + var attr interface{} + var ok bool + + if attr, ok = d.GetOk("name"); ok { + discovery_rule.Name = attr.(string) + } + + if attr, ok = d.GetOk("search"); ok { + discovery_rule.Search = attr.(string) + } + + if attr, ok = d.GetOk("hostgroup_ids"); ok { + discovery_rule.HostGroupId = attr.(int) + } + + if attr, ok = d.GetOk("hostname"); ok { + discovery_rule.Hostname = attr.(string) + } + + if attr, ok = d.GetOk("max_count"); ok { + discovery_rule.HostsLimitMaxCount = attr.(int) + } + + if attr, ok = d.GetOk("priority"); ok { + discovery_rule.Priority = attr.(int) + } + + if attr, ok = d.GetOk("enabled"); ok { + discovery_rule.Enabled = attr.(bool) + } + + if attr, ok = d.GetOk("location_ids"); ok { + attrSet := attr.(*schema.Set) + discovery_rule.LocationIds = conv.InterfaceSliceToIntSlice(attrSet.List()) + } + + if attr, ok = d.GetOk("organization_ids"); ok { + attrSet := attr.(*schema.Set) + discovery_rule.OrganizationIds = conv.InterfaceSliceToIntSlice(attrSet.List()) + } + + return &discovery_rule +} + +// buildForemanDiscoveryRuleResponse constructs a ForemanDiscoveryRuleResponse struct from a resource +// data reference. The struct's members are populated from the data populated +// in the resource data. Missing members will be left to the zero value for +// that member's type. +func buildForemanDiscoveryRuleResponse(d *schema.ResourceData) *api.ForemanDiscoveryRuleResponse { + log.Tracef("resource_foreman_discovery_rule.go#buildForemanDiscoveryRule") + + discovery_rule_response := api.ForemanDiscoveryRuleResponse{} + + obj := buildForemanObject(d) + discovery_rule_response.ForemanObject = *obj + + var attr interface{} + var ok bool + + if attr, ok = d.GetOk("name"); ok { + discovery_rule_response.Name = attr.(string) + } + + if attr, ok = d.GetOk("search"); ok { + discovery_rule_response.Search = attr.(string) + } + + if attr, ok = d.GetOk("hostgroup_ids"); ok { + discovery_rule_response.HostGroupId = attr.(int) + } + + if attr, ok = d.GetOk("hostname"); ok { + discovery_rule_response.Hostname = attr.(string) + } + + if attr, ok = d.GetOk("max_count"); ok { + discovery_rule_response.HostsLimitMaxCount = attr.(int) + } + + if attr, ok = d.GetOk("priority"); ok { + discovery_rule_response.Priority = attr.(int) + } + + if attr, ok = d.GetOk("enabled"); ok { + discovery_rule_response.Enabled = attr.(bool) + } + + if attr, ok = d.GetOk("locations"); ok { + attrSet := attr.(*schema.Set) + discovery_rule_response.Locations = make([]api.EntityResponse, attrSet.Len()) + for i, v := range attrSet.List() { + discovery_rule_response.Locations[i] = api.EntityResponse{ID: v.(int)} + } + } + + if attr, ok = d.GetOk("organizations"); ok { + attrSet := attr.(*schema.Set) + discovery_rule_response.Organizations = make([]api.EntityResponse, attrSet.Len()) + for i, v := range attrSet.List() { + discovery_rule_response.Organizations[i] = api.EntityResponse{ID: v.(int)} + } + } + + return &discovery_rule_response +} + +// setResourceDataFromForemanDiscoveryRule sets a ResourceData's attributes from +// the attributes of the supplied ForemanDiscoveryRule struct +func setResourceDataFromForemanDiscoveryRule(d *schema.ResourceData, fdr *api.ForemanDiscoveryRule) { + log.Tracef("resource_foreman_discovery_rule.go#setResourceDataFromForemanDiscoveryRule") + + d.SetId(strconv.Itoa(fdr.Id)) + d.Set("name", fdr.Name) + d.Set("search", fdr.Search) + d.Set("hostgroup_ids", fdr.HostGroupId) + d.Set("hostname", fdr.Hostname) + d.Set("max_count", fdr.HostsLimitMaxCount) + d.Set("priority", fdr.Priority) + d.Set("enabled", fdr.Enabled) + d.Set("location_ids", fdr.LocationIds) + d.Set("organization_ids", fdr.OrganizationIds) +} + +// setResourceDataFromForemanDiscoveryRule sets a ResourceData's attributes from +// the attributes of the supplied ForemanDiscoveryRule struct +func setResourceDataFromForemanDiscoveryRuleResponse(d *schema.ResourceData, fdr *api.ForemanDiscoveryRuleResponse) { + log.Tracef("resource_foreman_discovery_rule.go#setResourceDataFromForemanDiscoveryRule") + + d.SetId(strconv.Itoa(fdr.Id)) + d.Set("name", fdr.Name) + d.Set("search", fdr.Search) + d.Set("hostgroup_ids", fdr.HostGroupId) + d.Set("hostname", fdr.Hostname) + d.Set("hosts_limit", fdr.HostsLimitMaxCount) + d.Set("priority", fdr.Priority) + d.Set("enabled", fdr.Enabled) + + locationIDs := make([]int, 0, len(fdr.Locations)) + for _, location := range fdr.Locations { + locationIDs = append(locationIDs, location.ID) + } + d.Set("location_ids", locationIDs) + + organizationIDs := make([]int, 0, len(fdr.Organizations)) + for _, organization := range fdr.Organizations { + organizationIDs = append(organizationIDs, organization.ID) + } + d.Set("organization_ids", organizationIDs) +} + +// resourceForemanDiscoveryRuleCreate creates a ForemanDiscoveryRule resource +func resourceForemanDiscoveryRuleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("resource_foreman_discovery_rule.go#Create") + + client := meta.(*api.Client) + h := buildForemanDiscoveryRule(d) + + log.Debugf("ForemanDiscoveryRule: [%+v]", h) + + createdDiscoveryRule, createErr := client.CreateDiscoveryRule(ctx, h) + if createErr != nil { + return diag.FromErr(createErr) + } + + log.Debugf("Created ForemanDiscoveryRule: [%+v]", createdDiscoveryRule) + + setResourceDataFromForemanDiscoveryRule(d, createdDiscoveryRule) + + return nil +} + +// resourceForemanDiscoveryRuleRead reads a ForemanDiscoveryRule resource +func resourceForemanDiscoveryRuleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("resource_foreman_discovery_rule.go#Read") + + client := meta.(*api.Client) + h := buildForemanDiscoveryRuleResponse(d) + + log.Debugf("ForemanDiscoveryRule: [%+v]", h) + + readDiscoveryRule, readErr := client.ReadDiscoveryRule(ctx, h.Id) + if readErr != nil { + return diag.FromErr(api.CheckDeleted(d, readErr)) + } + + log.Debugf("Read ForemanDiscoveryRule: [%+v]", readDiscoveryRule) + + setResourceDataFromForemanDiscoveryRuleResponse(d, readDiscoveryRule) + fmt.Printf("Read ForemanDiscoveryRule: [%+v]\n", readDiscoveryRule) + + return nil +} + +// resourceForemanDiscoveryRuleUpdate updates a ForemanDiscoveryRule resource +func resourceForemanDiscoveryRuleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("resource_foreman_discovery_rule.go#Update") + + client := meta.(*api.Client) + h := buildForemanDiscoveryRule(d) + + log.Debugf("ForemanDiscoveryRule: [%+v]", h) + + updatedDiscoveryRule, updateErr := client.UpdateDiscoveryRule(ctx, h) + if updateErr != nil { + return diag.FromErr(updateErr) + } + + log.Debugf("Updated ForemanDiscoveryRule: [%+v]", updatedDiscoveryRule) + + setResourceDataFromForemanDiscoveryRule(d, updatedDiscoveryRule) + + return nil +} + +// resourceForemanDiscoveryRuleDelete deletes a ForemanDiscoveryRule resource +func resourceForemanDiscoveryRuleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Tracef("resource_foreman_discovery_rule.go#Delete") + + client := meta.(*api.Client) + h := buildForemanDiscoveryRule(d) + + log.Debugf("ForemanDiscoveryRule: [%+v]", h) + + // NOTE(ALL): d.SetId("") is automatically called by terraform assuming delete + // returns no errors + return diag.FromErr(api.CheckDeleted(d, client.DeleteDiscoveryRule(ctx, h.Id))) +} diff --git a/foreman/resource_foreman_discoveryrule_test.go b/foreman/resource_foreman_discoveryrule_test.go new file mode 100644 index 00000000..aa3b63c6 --- /dev/null +++ b/foreman/resource_foreman_discoveryrule_test.go @@ -0,0 +1,221 @@ +package foreman + +import ( + "math/rand" + "net/http" + "strconv" + "strings" + "testing" + + tfrand "github.com/HanseMerkur/terraform-provider-utils/rand" + "github.com/terraform-coop/terraform-provider-foreman/foreman/api" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const DiscoveryRuleURI = api.FOREMAN_API_URL_PREFIX + "/v2/discovery_rules" +const DiscoveryRuleTestDataPath = "testdata/3.11/discovery_rules" + +// ForemanDiscoveryRuleToInstanceState creates a mock instance state reference from a ForemanDiscoveryRule object +func ForemanDiscoveryRuleToInstanceState(obj api.ForemanDiscoveryRule) *terraform.InstanceState { + state := terraform.InstanceState{} + state.ID = strconv.Itoa(obj.Id) + // Build the attribute map from ForemanDiscoveryRule + attr := map[string]string{} + attr["name"] = obj.Name + attr["search"] = obj.Search + attr["hostgroup_id"] = strconv.Itoa(obj.Id) + attr["hostname"] = obj.Hostname + attr["max_count"] = strconv.Itoa(obj.HostsLimitMaxCount) + attr["priority"] = strconv.Itoa(obj.Priority) + attr["enabled"] = strconv.FormatBool(obj.Enabled) + attr["location_ids"] = intSliceToString(obj.LocationIds) + attr["hostgroup_ids"] = strconv.Itoa(obj.HostGroupId) + attr["organization_ids"] = intSliceToString(obj.OrganizationIds) + state.Attributes = attr + return &state +} + +// intSliceToString converts a slice of integers to a comma-separated string +func intSliceToString(slice []int) string { + strSlice := make([]string, len(slice)) + for i, v := range slice { + strSlice[i] = strconv.Itoa(v) + } + return strings.Join(strSlice, ",") +} + +// MockForemanDiscoveryRuleResourceData creates a mock ResourceData from InstanceState. +func MockForemanDiscoveryRuleResourceData(s *terraform.InstanceState) *schema.ResourceData { + r := resourceForemanDiscoveryRule() + return r.Data(s) +} + +// MockForemanDiscoveryRuleResourceDataFromFile creates a mock ResourceData from a JSON file +func MockForemanDiscoveryRuleResourceDataFromFile(t *testing.T, path string) *schema.ResourceData { + var obj api.ForemanDiscoveryRule + ParseJSONFile(t, path, &obj) + s := ForemanDiscoveryRuleToInstanceState(obj) + return MockForemanDiscoveryRuleResourceData(s) +} + +// RandForemanDiscoveryRule generates a random ForemanDiscoveryRule object +func RandForemanDiscoveryRule() api.ForemanDiscoveryRule { + obj := api.ForemanDiscoveryRule{} + + fo := RandForemanObject() + obj.ForemanObject = fo + + obj.Name = tfrand.String(20, tfrand.Lower+".") + + return obj +} + +// ForemanDiscoveryRuleResourceDataCompare compares two ResourceData references. +// If the two references differ in their attributes, the test will raise +// a fatal. +func ForemanDiscoveryRuleResourceDataCompare(t *testing.T, r1 *schema.ResourceData, r2 *schema.ResourceData) { + + // compare IDs + if r1.Id() != r2.Id() { + t.Fatalf( + "ResourceData references differ in Id. [%s], [%s]", + r1.Id(), + r2.Id(), + ) + } + + // build the attribute map + m := map[string]schema.ValueType{} + r := resourceForemanDiscoveryRule() + for key, value := range r.Schema { + m[key] = value.Type + } + + // compare the rest of the attributes + CompareResourceDataAttributes(t, m, r1, r2) + +} + +// TestSetResourceDataFromForemanDiscoveryRule ensures if ResourceData's attributes are correctly being set +func TestSetResourceDataFromForemanDiscoveryRule_Value(t *testing.T) { + + expectedObj := RandForemanDiscoveryRule() + expectedState := ForemanDiscoveryRuleToInstanceState(expectedObj) + expectedResourceData := MockForemanDiscoveryRuleResourceData(expectedState) + + actualObj := api.ForemanDiscoveryRule{} + actualState := ForemanDiscoveryRuleToInstanceState(actualObj) + actualResourceData := MockForemanDiscoveryRuleResourceData(actualState) + + setResourceDataFromForemanDiscoveryRule(actualResourceData, &expectedObj) + + ForemanDiscoveryRuleResourceDataCompare(t, actualResourceData, expectedResourceData) + +} + +// ResourceForemanDiscoveryRuleCreateTestCases Unit Test to check for correct URL and method +// SEE: foreman_api_test.go#TestCRUDFunction_CorrectURLAndMethod() +func ResourceForemanDiscoveryRuleCorrectURLAndMethodTestCases(t *testing.T) []TestCaseCorrectURLAndMethod { + + obj := api.ForemanDiscoveryRule{} + obj.Id = rand.Intn(100) + s := ForemanDiscoveryRuleToInstanceState(obj) + discovery_rulesURIById := DiscoveryRuleURI + "/" + strconv.Itoa(obj.Id) + + return []TestCaseCorrectURLAndMethod{ + { + TestCase: TestCase{ + funcName: "resourceForemanDiscoveryRuleRead", + crudFunc: resourceForemanDiscoveryRuleRead, + resourceData: MockForemanDiscoveryRuleResourceData(s), + }, + expectedURIs: []ExpectedUri{ + { + expectedURI: discovery_rulesURIById, + expectedMethod: http.MethodGet, + }, + }, + }, + } + +} + +// ResourceForemanDiscoveryRuleRequestDataEmptyTestCases Unit Test to check for empty request data +// SEE: foreman_api_test.go#TestCRUDFunction_RequestDataEmpty() +func ResourceForemanDiscoveryRuleRequestDataEmptyTestCases(t *testing.T) []TestCase { + + obj := api.ForemanDiscoveryRule{} + obj.Id = rand.Intn(100) + s := ForemanDiscoveryRuleToInstanceState(obj) + + return []TestCase{ + { + funcName: "resourceForemanDiscoveryRuleRead", + crudFunc: resourceForemanDiscoveryRuleRead, + resourceData: MockForemanDiscoveryRuleResourceData(s), + }, + } +} + +// SEE: foreman_api_test.go#TestCRUDFunction_StatusCodeError() +func ResourceForemanDiscoveryRuleStatusCodeTestCases(t *testing.T) []TestCase { + + obj := api.ForemanDiscoveryRule{} + obj.Id = rand.Intn(100) + s := ForemanDiscoveryRuleToInstanceState(obj) + + return []TestCase{ + { + funcName: "resourceForemanDiscoveryRuleRead", + crudFunc: resourceForemanDiscoveryRuleRead, + resourceData: MockForemanDiscoveryRuleResourceData(s), + }, + } +} + +// ResourceForemanDiscoveryRuleEmptyResponseTestCases Unit Test to check for empty response +// SEE: foreman_api_test.go#TestCRUDFunction_EmptyResponseError() +func ResourceForemanDiscoveryRuleEmptyResponseTestCases(t *testing.T) []TestCase { + obj := api.ForemanDiscoveryRule{} + obj.Id = rand.Intn(100) + s := ForemanDiscoveryRuleToInstanceState(obj) + + return []TestCase{ + { + funcName: "resourceForemanDiscoveryRuleRead", + crudFunc: resourceForemanDiscoveryRuleRead, + resourceData: MockForemanDiscoveryRuleResourceData(s), + }, + } +} + +// ResourceForemanDiscoveryRuleMockResponseTestCases Unit Test to check agains mock response +// SEE: foreman_api_test.go#TestCRUDFunction_MockResponse() +func ResourceForemanDiscoveryRuleMockResponseTestCases(t *testing.T) []TestCaseMockResponse { + + obj := RandForemanDiscoveryRule() + s := ForemanDiscoveryRuleToInstanceState(obj) + + return []TestCaseMockResponse{ + // If the server responds with a proper read response, the operation + // should succeed and the ResourceData's attributes should be updated + // to server's response + { + TestCase: TestCase{ + funcName: "resourceForemanDiscoveryRuleRead", + crudFunc: resourceForemanDiscoveryRuleRead, + resourceData: MockForemanDiscoveryRuleResourceData(s), + }, + responseFile: DiscoveryRuleTestDataPath + "/read_response.json", + returnError: false, + expectedResourceData: MockForemanDiscoveryRuleResourceDataFromFile( + t, + DiscoveryRuleTestDataPath+"/read_response.json", + ), + compareFunc: ForemanDiscoveryRuleResourceDataCompare, + }, + } + +} diff --git a/foreman/testdata/3.11/discovery_rules/create_response.json b/foreman/testdata/3.11/discovery_rules/create_response.json new file mode 100644 index 00000000..2242c52c --- /dev/null +++ b/foreman/testdata/3.11/discovery_rules/create_response.json @@ -0,0 +1,28 @@ +{ + "name": "test-discovery-rule-create-01 (create_response.json)", + "enabled": false, + "hostgroup_id": 1, + "hostgroup_name": "Example HPE Servers", + "hostname": "<%= @host.facts[nmprimary_dhcp4_option_host_name] %>", + "priority": 111, + "search": "name ~ create_example", + "hosts_limit": 0, + "id": 35, + "hosts": [], + "organizations": [ + { + "id": 1, + "name": "Default Organization", + "title": "Default Organization", + "description": null + } + ], + "locations": [ + { + "id": 2, + "name": "Default Location", + "title": "Default Location", + "description": null + } + ] +} diff --git a/foreman/testdata/3.11/discovery_rules/query_response_multi.json b/foreman/testdata/3.11/discovery_rules/query_response_multi.json new file mode 100644 index 00000000..b055bbf2 --- /dev/null +++ b/foreman/testdata/3.11/discovery_rules/query_response_multi.json @@ -0,0 +1,61 @@ +{ + "total": 2, + "subtotal": 2, + "page": 1, + "per_page": 20, + "search": null, + "sort": { + "by": null, + "order": null + }, + "results": [ + { + "name": "hpe-noble", + "enabled": true, + "hostgroup_id": 14, + "hostgroup_name": "HPE GreenLake/Noble", + "hostname": "<%= @host.facts['nmprimary_dhcp4_option_host_name'] %>", + "priority": 100, + "search": "facts.cmdline::fdi_os = noble and facts.bios_vendor = HPE", + "hosts_limit": 0, + "id": 1, + "hosts": [], + "organizations": [ + { + "id": 1, + "name": "Default Organization" + } + ], + "locations": [ + { + "id": 2, + "name": "Default Location" + } + ] + }, + { + "name": "hpe-jammy", + "enabled": false, + "hostgroup_id": 12, + "hostgroup_name": "HPE GreenLake/Jammy", + "hostname": "<%= @host.facts['nmprimary_dhcp4_option_host_name'] %>", + "priority": 200, + "search": "facts.cmdline::fdi_os = jammy and facts.bios_vendor = HPE", + "hosts_limit": 0, + "id": 2, + "hosts": [], + "organizations": [ + { + "id": 1, + "name": "Default Organization" + } + ], + "locations": [ + { + "id": 2, + "name": "Default Location" + } + ] + } + ] +} diff --git a/foreman/testdata/3.11/discovery_rules/query_response_single.json b/foreman/testdata/3.11/discovery_rules/query_response_single.json new file mode 100644 index 00000000..e53942aa --- /dev/null +++ b/foreman/testdata/3.11/discovery_rules/query_response_single.json @@ -0,0 +1,27 @@ +{ + "total": 20, + "subtotal": 1, + "page": 1, + "per_page": 20, + "search": "name=hpe-noble", + "sort": { + "by": null, + "order": null + }, + "results": [ + { + "name": "hpe-noble", + "enabled": true, + "hostgroup_id": 14, + "hostgroup_name": "HPE GreenLake/Noble", + "hostname": "<%= @host.facts['nmprimary_dhcp4_option_host_name'] %>", + "priority": 500, + "search": "facts.bios_vendor = HPE", + "hosts_limit": 0, + "id": 100, + "hosts": [], + "organizations": [{ "id": 1, "name": "Default Organization" }], + "locations": [{ "id": 2, "name": "Default Location" }] + } + ] +} diff --git a/foreman/testdata/3.11/discovery_rules/query_response_zero.json b/foreman/testdata/3.11/discovery_rules/query_response_zero.json new file mode 100644 index 00000000..164b0b32 --- /dev/null +++ b/foreman/testdata/3.11/discovery_rules/query_response_zero.json @@ -0,0 +1,12 @@ +{ + "total": 21, + "subtotal": 0, + "page": 1, + "per_page": 20, + "search": "name=test-discovery-rule-99", + "sort": { + "by": null, + "order": null + }, + "results": [] +} diff --git a/foreman/testdata/3.11/discovery_rules/read_response.json b/foreman/testdata/3.11/discovery_rules/read_response.json new file mode 100644 index 00000000..16e36d03 --- /dev/null +++ b/foreman/testdata/3.11/discovery_rules/read_response.json @@ -0,0 +1,28 @@ +{ + "id": 1, + "name": "Example Discovery Rule (read_response.json)", + "search": "name ~ example2", + "hostname": "example-host-1", + "hostgroup_id": 2, + "hosts_limit": 200, + "priority": 10, + "enabled": false, + "created_at": "2023-10-01T12:00:00Z", + "updated_at": "2023-10-01T12:00:00Z", + "locations": [ + { + "id": 1, + "name": "DC1", + "title": "DC1", + "description": null + } + ], + "organizations": [ + { + "id": 1, + "name": "MockCompany", + "title": "MockCompany", + "description": null + } + ] +} diff --git a/foreman/testdata/3.11/discovery_rules/update_response.json b/foreman/testdata/3.11/discovery_rules/update_response.json new file mode 100644 index 00000000..df2d848b --- /dev/null +++ b/foreman/testdata/3.11/discovery_rules/update_response.json @@ -0,0 +1,28 @@ +{ + "id": 1, + "name": "Example Discovery Rule (update_response.json)", + "search": "name ~ example", + "hostname": "example-host-1", + "hostgroup_id": 2, + "max_count": 0, + "priority": 100, + "enabled": true, + "created_at": "2023-10-01T12:00:00Z", + "updated_at": "2024-10-01T12:00:00Z", + "locations": [ + { + "id": 2, + "name": "DC2", + "title": "DC2", + "description": null + } + ], + "organizations": [ + { + "id": 2, + "name": "MockCompany2", + "title": "MockCompany2", + "description": null + } + ] +}