Skip to content

Commit

Permalink
fix: macos plist handling for payloadUUID update operation (#562)
Browse files Browse the repository at this point in the history
* feat: enhance macOS Configuration Profile update to preserve existing UUIDs

* feat: implement UUID handling for macOS Configuration Profile updates
  • Loading branch information
ShocOne authored Jan 6, 2025
1 parent 2c114c8 commit a506164
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 58 deletions.
7 changes: 1 addition & 6 deletions internal/resources/filesharedistributionpoints/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@ const (
JamfProResourceDistributionPoint = "Distribution Point"
)

// Create requires a mutex need to lock Create requests during parallel runs
// var mu sync.Mutex

// create is responsible for creating a new file share
func create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// mu.Lock()
// defer mu.Unlock()
return common.Create(
ctx,
d,
Expand Down Expand Up @@ -64,7 +59,7 @@ func update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.
)
}

// deleteis responsible for deleting a Jamf Pro file share distribution point from the remote system.
// delete is responsible for deleting a Jamf Pro file share distribution point from the remote system.
func delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return common.Delete(
ctx,
Expand Down
73 changes: 71 additions & 2 deletions internal/resources/macosconfigurationprofilesplist/constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,58 @@
package macosconfigurationprofilesplist

import (
"bytes"
"encoding/xml"
"fmt"
"html"
"log"
"strings"

"github.com/deploymenttheory/go-api-sdk-jamfpro/sdk/jamfpro"
"github.com/deploymenttheory/terraform-provider-jamfpro/internal/resources/common/sharedschemas"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"howett.net/plist"
)

// constructJamfProMacOSConfigurationProfilePlist constructs a ResourceMacOSConfigurationProfile object from the provided schema data.
func constructJamfProMacOSConfigurationProfilePlist(d *schema.ResourceData) (*jamfpro.ResourceMacOSConfigurationProfile, error) {
// constructJamfProMacOSConfigurationProfilePlist constructs a ResourceMacOSConfigurationProfile object from schema data.
// It supports two modes:
// - create: Builds profile from schema data only
// - update: Fetches existing profile from Jamf Pro, extracts PayloadUUID/PayloadIdentifier values from existing plist,
// injects them into the new plist to maintain UUID continuity
//
// The function:
// 1. For update mode:
// - Retrieves existing profile from Jamf Pro API
// - Decodes existing plist to extract UUIDs
//
// 2. Constructs base profile from schema data (name, description, etc)
// 3. Builds scope and self-service sections if configured
// 4. For update mode:
// - Maps existing UUIDs by PayloadDisplayName
// - Updates PayloadUUID/PayloadIdentifier in new plist to match existing
// - Re-encodes updated plist
//
// Parameters:
// - d: Schema ResourceData containing configuration
// - mode: "create" or "update" to control UUID handling
// - meta: Provider meta containing client for API calls
//
// Returns:
// - Constructed ResourceMacOSConfigurationProfile
// - Error if construction or API calls fail
func constructJamfProMacOSConfigurationProfilePlist(d *schema.ResourceData, mode string, meta interface{}) (*jamfpro.ResourceMacOSConfigurationProfile, error) {
var existingProfile *jamfpro.ResourceMacOSConfigurationProfile

if mode == "update" {
client := meta.(*jamfpro.Client)
resourceID := d.Id()
var err error
existingProfile, err = client.GetMacOSConfigurationProfileByID(resourceID)
if err != nil {
return nil, fmt.Errorf("failed to get existing profile: %v", err)
}
}

resource := &jamfpro.ResourceMacOSConfigurationProfile{
General: jamfpro.MacOSConfigurationProfileSubsetGeneral{
Name: d.Get("name").(string),
Expand All @@ -40,6 +80,35 @@ func constructJamfProMacOSConfigurationProfilePlist(d *schema.ResourceData) (*ja
resource.SelfService = constructMacOSConfigurationProfileSubsetSelfService(selfServiceData)
}

// Handle UUID injection for update operations
if mode == "update" && existingProfile != nil {
uuidMap := make(map[string]string)
var existingPlist map[string]interface{}
existingPayload := html.UnescapeString(existingProfile.General.Payloads)
if err := plist.NewDecoder(strings.NewReader(existingPayload)).Decode(&existingPlist); err != nil {
return nil, fmt.Errorf("failed to decode existing plist: %v", err)
}

extractUUIDs(existingPlist, uuidMap)

var newPlist map[string]interface{}
newPayload := html.UnescapeString(resource.General.Payloads)
if err := plist.NewDecoder(strings.NewReader(newPayload)).Decode(&newPlist); err != nil {
return nil, fmt.Errorf("failed to decode new plist: %v", err)
}

updateUUIDs(newPlist, uuidMap)

var buf bytes.Buffer
encoder := plist.NewEncoder(&buf)
encoder.Indent(" ")
if err := encoder.Encode(newPlist); err != nil {
return nil, fmt.Errorf("failed to encode updated plist: %v", err)
}

resource.General.Payloads = html.EscapeString(buf.String())
}

resourceXML, err := xml.MarshalIndent(resource, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal Jamf Pro macOS Configuration Profile '%s' to XML: %v", resource.General.Name, err)
Expand Down
46 changes: 20 additions & 26 deletions internal/resources/macosconfigurationprofilesplist/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,17 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// Create requires a mutex need to lock Create requests during parallel runs
// var mu sync.Mutex

// resourceJamfProMacOSConfigurationProfilesPlistCreate is responsible for creating a new Jamf Pro macOS Configuration Profile in the remote system.
// create is responsible for creating a new Jamf Pro macOS Configuration Profile in the remote system.
// The function:
// 1. Constructs the attribute data using the provided Terraform configuration.
// 2. Calls the API to create the attribute in Jamf Pro.
// 3. Updates the Terraform state with the ID of the newly created attribute.
// 4. Initiates a read operation to synchronize the Terraform state with the actual state in Jamf Pro.
func resourceJamfProMacOSConfigurationProfilesPlistCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*jamfpro.Client)
var diags diag.Diagnostics

// mu.Lock()
// defer mu.Unlock()

resource, err := constructJamfProMacOSConfigurationProfilePlist(d)
resource, err := constructJamfProMacOSConfigurationProfilePlist(d, "create", meta)
if err != nil {
return diag.FromErr(fmt.Errorf("failed to construct Jamf Pro macOS Configuration Profile: %v", err))
}
Expand All @@ -49,15 +43,15 @@ func resourceJamfProMacOSConfigurationProfilesPlistCreate(ctx context.Context, d

d.SetId(strconv.Itoa(creationResponse.ID))

return append(diags, resourceJamfProMacOSConfigurationProfilesPlistReadNoCleanup(ctx, d, meta)...)
return append(diags, readNoCleanup(ctx, d, meta)...)
}

// resourceJamfProMacOSConfigurationProfilesPlistRead is responsible for reading the current state of a Jamf Pro config profile Resource from the remote system.
// read is responsible for reading the current state of a Jamf Pro config profile Resource from the remote system.
// The function:
// 1. Fetches the attribute's current state using its ID. If it fails then obtain attribute's current state using its Name.
// 2. Updates the Terraform state with the fetched data to ensure it accurately reflects the current state in Jamf Pro.
// 3. Handles any discrepancies, such as the attribute being deleted outside of Terraform, to keep the Terraform state synchronized.
func resourceJamfProMacOSConfigurationProfilesPlistRead(ctx context.Context, d *schema.ResourceData, meta interface{}, cleanup bool) diag.Diagnostics {
func read(ctx context.Context, d *schema.ResourceData, meta interface{}, cleanup bool) diag.Diagnostics {
client := meta.(*jamfpro.Client)
resourceID := d.Id()
var diags diag.Diagnostics
Expand All @@ -79,25 +73,25 @@ func resourceJamfProMacOSConfigurationProfilesPlistRead(ctx context.Context, d *
return append(diags, updateState(d, response)...)
}

// resourceJamfProMacOSConfigurationProfilesPlistReadWithCleanup reads the resource with cleanup enabled
func resourceJamfProMacOSConfigurationProfilesPlistReadWithCleanup(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return resourceJamfProMacOSConfigurationProfilesPlistRead(ctx, d, meta, true)
// readWithCleanup reads the resource with cleanup enabled
func readWithCleanup(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return read(ctx, d, meta, true)
}

// resourceJamfProMacOSConfigurationProfilesPlistReadNoCleanup reads the resource with cleanup disabled
func resourceJamfProMacOSConfigurationProfilesPlistReadNoCleanup(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return resourceJamfProMacOSConfigurationProfilesPlistRead(ctx, d, meta, false)
// readNoCleanup reads the resource with cleanup disabled
func readNoCleanup(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return read(ctx, d, meta, false)
}

// resourceJamfProMacOSConfigurationProfilesPlistUpdate is responsible for updating an existing Jamf Pro config profile on the remote system.
func resourceJamfProMacOSConfigurationProfilesPlistUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// update is responsible for updating an existing Jamf Pro config profile on the remote system.
func update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*jamfpro.Client)
var diags diag.Diagnostics
resourceID := d.Id()

resource, err := constructJamfProMacOSConfigurationProfilePlist(d)
resource, err := constructJamfProMacOSConfigurationProfilePlist(d, "update", meta)
if err != nil {
return diag.FromErr(fmt.Errorf("failed to construct Jamf Pro macOS Configuration Profile for update: %v", err))
return diag.FromErr(fmt.Errorf("failed to construct profile for update: %v", err))
}

err = retry.RetryContext(ctx, d.Timeout(schema.TimeoutUpdate), func() *retry.RetryError {
Expand All @@ -109,14 +103,14 @@ func resourceJamfProMacOSConfigurationProfilesPlistUpdate(ctx context.Context, d
})

if err != nil {
return diag.FromErr(fmt.Errorf("failed to update Jamf Pro macOS Configuration Profile '%s' (ID: %s) after retries: %v", resource.General.Name, resourceID, err))
return diag.FromErr(fmt.Errorf("failed to update profile '%s' (ID: %s): %v", resource.General.Name, resourceID, err))
}

return append(diags, resourceJamfProMacOSConfigurationProfilesPlistReadNoCleanup(ctx, d, meta)...)
return append(diags, readNoCleanup(ctx, d, meta)...)
}

// resourceJamfProMacOSConfigurationProfilesPlistDelete is responsible for deleting a Jamf Pro config profile.
func resourceJamfProMacOSConfigurationProfilesPlistDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// delete is responsible for deleting a Jamf Pro config profile.
func delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*jamfpro.Client)
var diags diag.Diagnostics
resourceID := d.Id()
Expand Down
55 changes: 55 additions & 0 deletions internal/resources/macosconfigurationprofilesplist/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,58 @@ func FixDuplicateNotificationKey(resp *jamfpro.ResourceMacOSConfigurationProfile
// Return default value if no valid boolean value is found
return false, nil
}

// extractUUIDs recursively extracts config profile UUIDs from a plist structure
// and stores them in a map by PayloadDisplayName.
func extractUUIDs(data interface{}, uuidMap map[string]string) {
switch v := data.(type) {
case map[string]interface{}:
displayName, hasDisplayName := v["PayloadDisplayName"].(string)
uuid, hasUUID := v["PayloadUUID"].(string)

if hasDisplayName && hasUUID {
uuidMap[displayName] = uuid
} else if hasUUID {
// For root level, use special key
uuidMap["root"] = uuid
}

// Recursively process all values
for _, val := range v {
extractUUIDs(val, uuidMap)
}
case []interface{}:
for _, item := range v {
extractUUIDs(item, uuidMap)
}
}
}

// updateUUIDs recursively updates config profile UUIDs in a
// plist structure
func updateUUIDs(data interface{}, uuidMap map[string]string) {
switch v := data.(type) {
case map[string]interface{}:
displayName, hasDisplayName := v["PayloadDisplayName"].(string)
if hasDisplayName {
if uuid, exists := uuidMap[displayName]; exists {
v["PayloadUUID"] = uuid
v["PayloadIdentifier"] = uuid // Also update identifier
}
} else {
// For root level
if uuid, exists := uuidMap["root"]; exists {
v["PayloadUUID"] = uuid
}
}

// Recursively process all values
for _, val := range v {
updateUUIDs(val, uuidMap)
}
case []interface{}:
for _, item := range v {
updateUUIDs(item, uuidMap)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (
// resourceJamfProMacOSConfigurationProfilesPlist defines the schema and CRUD operations for managing Jamf Pro macOS Configuration Profiles in Terraform.
func ResourceJamfProMacOSConfigurationProfilesPlist() *schema.Resource {
return &schema.Resource{
CreateContext: resourceJamfProMacOSConfigurationProfilesPlistCreate,
ReadContext: resourceJamfProMacOSConfigurationProfilesPlistReadWithCleanup,
UpdateContext: resourceJamfProMacOSConfigurationProfilesPlistUpdate,
DeleteContext: resourceJamfProMacOSConfigurationProfilesPlistDelete,
CreateContext: create,
ReadContext: readWithCleanup,
UpdateContext: update,
DeleteContext: delete,
CustomizeDiff: mainCustomDiffFunc,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(120 * time.Second),
Expand Down
21 changes: 1 addition & 20 deletions internal/resources/macosconfigurationprofilesplist/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,18 +276,6 @@ func setSelfService(selfService jamfpro.MacOSConfigurationProfileSubsetSelfServi
selfServiceBlock["notification"] = correctNotifValue
}

// Handle self service icon
if selfService.SelfServiceIcon.ID != 0 {
selfServiceBlock["self_service_icon"] = []interface{}{
map[string]interface{}{
"id": selfService.SelfServiceIcon.ID,
"uri": selfService.SelfServiceIcon.URI,
"data": selfService.SelfServiceIcon.Data,
"filename": selfService.SelfServiceIcon.Filename,
},
}
}

// Handle self service categories
if len(selfService.SelfServiceCategories) > 0 {
categories := make([]interface{}, len(selfService.SelfServiceCategories))
Expand All @@ -305,7 +293,7 @@ func setSelfService(selfService jamfpro.MacOSConfigurationProfileSubsetSelfServi
// Check if all values are default
allDefault := true
for key, value := range selfServiceBlock {
if !reflect.DeepEqual(value, defaults[key]) {
if defaultVal, ok := defaults[key]; ok && !reflect.DeepEqual(value, defaultVal) {
allDefault = false
break
}
Expand All @@ -316,13 +304,6 @@ func setSelfService(selfService jamfpro.MacOSConfigurationProfileSubsetSelfServi
return nil, nil
}

// Remove default values
for key, value := range selfServiceBlock {
if reflect.DeepEqual(value, defaults[key]) {
delete(selfServiceBlock, key)
}
}

log.Println("Initializing self service in state")
log.Printf("Final state self service: %+v\n", selfServiceBlock)

Expand Down

0 comments on commit a506164

Please sign in to comment.