diff --git a/go.mod b/go.mod index 70b9d1f..3177e86 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/terraform-plugin-sdk/v2 v2.4.3 github.com/mitchellh/go-homedir v1.1.0 - github.com/yohan460/go-jamf-api v0.0.0-20221116181400-9279a31bfd4c + github.com/yohan460/go-jamf-api v0.0.0-20230411151701-d1c2c274032b ) require ( diff --git a/go.sum b/go.sum index c3f550b..ec71644 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -github.com/yohan460/go-jamf-api v0.0.0-20221116181400-9279a31bfd4c h1:8hHLXtVHpCxV0vIvT09RZbrRWJrkVt7lhh7nnor+c+c= -github.com/yohan460/go-jamf-api v0.0.0-20221116181400-9279a31bfd4c/go.mod h1:tBRMKJ3SjrouWaToIt9vqf7R63qtoi3UeQ486vcYz38= +github.com/yohan460/go-jamf-api v0.0.0-20230411151701-d1c2c274032b h1:4EkYhRnTZxYkUYWhQySBA0wxdKGBioUJ8MB0gkyhNQE= +github.com/yohan460/go-jamf-api v0.0.0-20230411151701-d1c2c274032b/go.mod h1:tBRMKJ3SjrouWaToIt9vqf7R63qtoi3UeQ486vcYz38= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/jamf/data_source_jamf_computer_extension_attribute.go b/jamf/data_source_jamf_computer_extension_attribute.go new file mode 100644 index 0000000..cff2eef --- /dev/null +++ b/jamf/data_source_jamf_computer_extension_attribute.go @@ -0,0 +1,164 @@ +package jamf + +import ( + "context" + "strconv" + + "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" + "github.com/yohan460/go-jamf-api" +) + +func dataSourceJamfComputerExtensionAttribute() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceJamfComputerExtensionAttributeRead, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "data_type": { + Type: schema.TypeString, + Optional: true, + Default: "String", + ValidateFunc: validation.StringInSlice([]string{"String", "Integer", "Date"}, false), + }, + "inventory_display": { + Type: schema.TypeString, + Optional: true, + Default: "Extension Attributes", + ValidateFunc: validation.StringInSlice([]string{"General", "Hardware", "Operating System", "User and Location", "Purchasing", "Extension Attributes"}, false), + }, + "script": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{"script", "text_field", "popup_menu"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "platform": { + Type: schema.TypeString, + Optional: true, + Default: "Mac", + ValidateFunc: validation.StringInSlice([]string{"Mac", "Windows"}, false), + }, + "script_contents": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"script.0.file_path"}, + }, + "file_path": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"script.0.script_contents"}, + }, + }, + }, + }, + "text_field": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // set as a placeholder to `text_field` is recognized, + // this schema is not used anywhere + "input_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "popup_menu": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "choices": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceJamfComputerExtensionAttributeRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + c := m.(*jamf.Client) + + resp, err := c.GetComputerExtensionAttributeByName(d.Get("name").(string)) + if err != nil { + return diag.FromErr(err) + } + + deconstructJamfComputerExtensionAttributeStruct(d, resp) + + return diags +} + +func deconstructJamfComputerExtensionAttributeStruct(d *schema.ResourceData, in *jamf.ComputerExtensionAttribute) { + d.SetId(strconv.Itoa(in.Id)) + d.Set("name", in.Name) + d.Set("description", in.Description) + d.Set("data_type", in.DataType) + d.Set("inventory_display", in.InventoryDisplay) + d.Set("recon_display", in.ReconDisplay) + + // Input Type + switch inputType := in.InputType.Type; inputType { + case "script": + scriptInterface := map[string]interface{}{ + "enabled": in.Enabled, + "platform": in.InputType.Platform, + } + + if s, ok := d.GetOk("script"); ok { + for _, v := range s.([]interface{}) { + script := v.(map[string]interface{}) + + // since file_path is always set in TypeList + if script["file_path"] == "" { + scriptInterface["script_contents"] = in.InputType.Script + } + } + } + + d.Set("script", scriptInterface) + case "Text Field": + d.Set("text_field", []interface{}{ + map[string]interface{}{ + "input_type": "text_field", + }, + }) + case "Pop-up Menu": + d.Set("popup_menu", []interface{}{ + map[string]interface{}{ + "choices": in.InputType.Choices, + }, + }) + } + + return +} diff --git a/jamf/provider.go b/jamf/provider.go index 9befc87..43161c8 100644 --- a/jamf/provider.go +++ b/jamf/provider.go @@ -44,6 +44,7 @@ func Provider() *schema.Provider { "jamf_script": resourceJamfScript(), "jamf_policy": resourceJamfPolicy(), "jamf_computer_configuration_profile": resourceJamfComputerConfigurationProfile(), + "jamf_computer_extension_attribute": resourceJamfComputerExtensionAttribute(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -56,6 +57,7 @@ func Provider() *schema.Provider { "jamf_package": dataSourceJamfPackage(), "jamf_policy": dataSourceJamfPolicy(), "jamf_computer_configuration_profile": dataSourceJamfComputerConfigurationProfile(), + "jamf_computer_extension_attribute": dataSourceJamfComputerExtensionAttribute(), }, ConfigureContextFunc: providerConfigure, } diff --git a/jamf/resource_jamf_computer_extension_attribute.go b/jamf/resource_jamf_computer_extension_attribute.go new file mode 100644 index 0000000..b816fba --- /dev/null +++ b/jamf/resource_jamf_computer_extension_attribute.go @@ -0,0 +1,287 @@ +package jamf + +import ( + "context" + "fmt" + "strconv" + + "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" + "github.com/yohan460/go-jamf-api" +) + +func resourceJamfComputerExtensionAttribute() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceJamfComputerExtensionAttributeCreate, + ReadContext: resourceJamfComputerExtensionAttributeRead, + UpdateContext: resourceJamfComputerExtensionAttributeUpdate, + DeleteContext: resourceJamfComputerExtensionAttributeDelete, + Importer: &schema.ResourceImporter{ + StateContext: importJamfComputerExtensionAttributeState, + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "data_type": { + Type: schema.TypeString, + Optional: true, + Default: "String", + ValidateFunc: validation.StringInSlice([]string{"String", "Integer", "Date"}, false), + }, + "inventory_display": { + Type: schema.TypeString, + Optional: true, + Default: "Extension Attributes", + ValidateFunc: validation.StringInSlice([]string{"General", "Hardware", "Operating System", "User and Location", "Purchasing", "Extension Attributes"}, false), + }, + "script": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{"script", "text_field", "popup_menu"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "platform": { + Type: schema.TypeString, + Optional: true, + Default: "Mac", + ValidateFunc: validation.StringInSlice([]string{"Mac", "Windows"}, false), + }, + "script_contents": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"script.0.file_path"}, + }, + "file_path": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"script.0.script_contents"}, + }, + }, + }, + }, + "text_field": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // set as a placeholder to `text_field` is recognized, + // this schema is not used anywhere + "input_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "popup_menu": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "choices": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + } +} + +func buildJamfComputerExtensionAttributeStruct(d *schema.ResourceData) (*jamf.ComputerExtensionAttribute, error) { + var out jamf.ComputerExtensionAttribute + + id, _ := strconv.Atoi(d.Id()) + out.Id = id + + out.Name = d.Get("name").(string) + + if v, ok := d.GetOk("description"); ok { + out.Description = v.(string) + } + + if v, ok := d.GetOk("data_type"); ok { + out.DataType = v.(string) + } + + if s, ok := d.GetOk("script"); ok { + for _, v := range s.([]interface{}) { + script := v.(map[string]interface{}) + + out.InputType.Type = "script" + + if v, ok := script["enabled"]; ok { + out.Enabled = v.(bool) + } + + if v, ok := script["platform"]; ok { + out.InputType.Platform = v.(string) + } + + filePath, hasFilePath := script["file_path"] + if hasFilePath { + if filePath == "" { + hasFilePath = false // since file_path is always set in TypeList + } + } + scriptContents, hasScriptContents := script["script_contents"] + if hasScriptContents { + if scriptContents == "" { + hasScriptContents = false // since script_contents is always set in TypeList + } + } + + if hasFilePath && !hasScriptContents { + content, err := loadFileContent(filePath.(string)) + if err != nil { + return &out, err + } + out.InputType.Script = content + } else if !hasFilePath && hasScriptContents && scriptContents != "" { + out.InputType.Script = scriptContents.(string) + } else { + return &out, fmt.Errorf("only one of file_path and script_contents must be set") + } + } + } + + if _, ok := d.GetOk("text_field"); ok { + out.InputType.Type = "Text Field" + } + + if s, ok := d.GetOk("popup_menu"); ok { + val := s.(*schema.Set).List() + popup := val[0].(map[string]interface{}) + + out.InputType.Type = "Pop-up Menu" + + if v, ok := popup["choices"]; ok { + choices := v.([]interface{}) + choicesList := make([]string, len(choices)) + for i, c := range choices { + choicesList[i] = c.(string) + } + out.InputType.Choices = choicesList + } + } + + if v, ok := d.GetOk("inventory_display"); ok { + out.InventoryDisplay = v.(string) + } + + if v, ok := d.GetOk("recon_display"); ok { + out.ReconDisplay = v.(string) + } + + return &out, nil +} + +func resourceJamfComputerExtensionAttributeCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*jamf.Client) + + b, err := buildJamfComputerExtensionAttributeStruct(d) + if err != nil { + return diag.FromErr(err) + } + + id, err := c.CreateComputerExtensionAttribute(b) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(id)) + + return resourceJamfComputerExtensionAttributeRead(ctx, d, m) +} + +func resourceJamfComputerExtensionAttributeRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + c := m.(*jamf.Client) + + id, _ := strconv.Atoi(d.Id()) + resp, err := c.GetComputerExtensionAttribute(id) + + if err != nil { + if jamfErr, ok := err.(jamf.Error); ok && jamfErr.StatusCode() == 404 { + d.SetId("") + } else { + return diag.FromErr(err) + } + } else { + deconstructJamfComputerExtensionAttributeStruct(d, resp) + } + + return diags +} + +func resourceJamfComputerExtensionAttributeUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*jamf.Client) + + b, err := buildJamfComputerExtensionAttributeStruct(d) + if err != nil { + return diag.FromErr(err) + } + + if _, err := c.UpdateComputerExtensionAttribute(b); err != nil { + return diag.FromErr(err) + } + + return resourceJamfComputerExtensionAttributeRead(ctx, d, m) +} + +func resourceJamfComputerExtensionAttributeDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + c := m.(*jamf.Client) + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if _, err := c.DeleteComputerExtensionAttribute(id); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func importJamfComputerExtensionAttributeState(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + c := m.(*jamf.Client) + d.SetId(d.Id()) + id, err := strconv.Atoi(d.Id()) + if err != nil { + return nil, err + } + resp, err := c.GetComputerExtensionAttribute(id) + if err != nil { + return nil, fmt.Errorf("cannot get computer extension attribute data") + } + + deconstructJamfComputerExtensionAttributeStruct(d, resp) + + return []*schema.ResourceData{d}, nil +} diff --git a/jamf/resource_jamf_computer_extension_attribute_test.go b/jamf/resource_jamf_computer_extension_attribute_test.go new file mode 100644 index 0000000..27b978d --- /dev/null +++ b/jamf/resource_jamf_computer_extension_attribute_test.go @@ -0,0 +1,78 @@ +package jamf + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccJamfComputerExtensionAttribute_basic(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckJamfComputerExtensionAttributeScript, + Check: resource.ComposeTestCheckFunc( + testAccCheckJamfComputerExtensionAttributeExists("jamf_computer_extension_attribute.extensionattribute-script"), + ), + }, + { + Config: testAccCheckJamfComputerExtensionAttributeTextField, + Check: resource.ComposeTestCheckFunc( + testAccCheckJamfComputerExtensionAttributeExists("jamf_computer_extension_attribute.extensionattribute-textfield"), + ), + }, + { + Config: testAccCheckJamfComputerExtensionAttributePopup, + Check: resource.ComposeTestCheckFunc( + testAccCheckJamfComputerExtensionAttributeExists("jamf_computer_extension_attribute.extensionattribute-popup"), + ), + }, + }, + }) +} + +const ( + testAccCheckJamfComputerExtensionAttributeScript = `resource "jamf_computer_extension_attribute" "extensionattribute-script" { + name = "Terraform test script" + description = "testing jamf extension attribute resource" + data_type = "string" + inventory_display = "Extension Attributes" + + script { + enabled = false + script_contents = "#!/bin/bash\nprint(\"hello world\")" + } + }` + + testAccCheckJamfComputerExtensionAttributeTextField = `resource "jamf_computer_extension_attribute" "extensionattribute-textfield" { + name = "Terraform test textfield" + text_field {} + }` + + testAccCheckJamfComputerExtensionAttributePopup = `resource "jamf_computer_extension_attribute" "extensionattribute-popup" { + name = "Terraform test popup" + popup_menu { + choices = ["choice1", "choice2"] + } + }` +) + +func testAccCheckJamfComputerExtensionAttributeExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + extensionattribute, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if extensionattribute.Primary.ID == "" { + return fmt.Errorf("No resource id set") + } + + return nil + } +} diff --git a/website/docs/d/computerComputerExtensionAttribute.html.md b/website/docs/d/computerComputerExtensionAttribute.html.md new file mode 100644 index 0000000..553ebb0 --- /dev/null +++ b/website/docs/d/computerComputerExtensionAttribute.html.md @@ -0,0 +1,44 @@ +--- +layout: "jamf" +subcategory: "Data Sources" +page_title: "Jamf: jamf_computer_extension_attribute" +description: |- + Provides details about a computer extension attribute. +--- + +# Data Source: jamf_computer_extension_attribute + +Use this data source to get the computer extension attribute information. + +The computer extension attribute data source allows access to details of a specific computer extension attribute within Jamf. + +## Example Usage + +```hcl +resource "jamf_computer_extension_attribute" "test-extension-attribute-script" { + name = "test-extension-attribute-script" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Name of the extension attribute. + +## Attributes Reference + +In addition to the above arguments, the following attributes are exported: + +* `id` - ID of the extension attribute. +* `description` - (Optional) Description of the extension attribute. +* `data_type` - (Optional) The type of data collected, defaults to `String` +* `inventory_display` - (Optional) Where the extension attribute is displayed in Jamf, defaults to `Extension Attributes`. +* `script` - (Optional) Run a script to collect inventory data. + * `enabled` - (Optional) Enables collecting inventory data, defaults to `true`. + * `platform` - (Optional) Not documented, defaults to `Mac`. + * `script_contents` - (Optional) The contents of the script. + * `file_path` - (Optional) The path of a file containing the script contents. +* `text_field` - (Optional) Display a text field to collect inventory data. +* `popup_menu` - (Optional) Display a pop-up menu to collect inventory data. + * `choices` - (Optional) List of values in the popup menu. diff --git a/website/docs/r/computerComputerExtensionAttribute.html.md b/website/docs/r/computerComputerExtensionAttribute.html.md new file mode 100644 index 0000000..f392a0f --- /dev/null +++ b/website/docs/r/computerComputerExtensionAttribute.html.md @@ -0,0 +1,69 @@ +--- +layout: "jamf" +subcategory: "Resources" +page_title: "Jamf: jamf_computer_extension_attribute" +description: |- + Provides details about a computer extension attribute. +--- + +# Resource: jamf_computer_extension_attribute + +Provides a computer extension attribute. + +## Example Usage + +```hcl +resource "jamf_computer_extension_attribute" "test-extension-attribute-script" { + name = "test-extension-attribute-script" + script { + script_contents = file("${path.module}/extension-attributes/script-1.sh") + } +} +``` + +```hcl +resource "jamf_computer_extension_attribute" "test-extension-attribute-text-field" { + name = "test-extension-attribute-text-field" + text_field { } +} +``` + +```hcl +resource "jamf_computer_extension_attribute" "test-extension-attribute-popup-menu" { + name = "test-extension-attribute-popup-menu" + popup_menu { + choices = ["choice1", "choice2"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Name of the extension attribute. +* `description` - (Optional) Description of the extension attribute. +* `data_type` - (Optional) The type of data collected, defaults to `String` +* `inventory_display` - (Optional) Where the extension attribute is displayed in Jamf, defaults to `Extension Attributes`. +* `script` - (Optional) Run a script to collect inventory data. + * `enabled` - (Optional) Enables collecting inventory data, defaults to `true`. + * `platform` - (Optional) Not documented, defaults to `Mac`. + * `script_contents` - (Optional) The contents of the script. + * `file_path` - (Optional) The path of a file containing the script contents. +* `text_field` - (Optional) Display a text field to collect inventory data. +* `popup_menu` - (Optional) Display a pop-up menu to collect inventory data. + * `choices` - (Optional) List of values in the popup menu. + +Only one of script, text_field, or popup_menu is required in a resource block. + +## Attributes Reference + +In addition to the above arguments, the following attributes are exported: + +* `id` - ID of the extension attribute. +* `text_field` - Display a text field to collect inventory data. + * `input_type` - Placeholder for an otherwise empty block, not used. + +## Notes + +When using the `file_path` parameter the file name must be changed to trigger a Terraform update. \ No newline at end of file