Migrate Genesys Cloud User Resources for Terraform Provider v1.35.0 Schema Changes

Migrate Genesys Cloud User Resources for Terraform Provider v1.35.0 Schema Changes

What You Will Build

  • You will update existing Terraform Infrastructure as Code (IaC) scripts to align with the breaking schema changes in the genesyscloud_user resource introduced in provider version 1.35.0.
  • You will use the Genesys Cloud Terraform Provider to manage user identities, specifically handling the new email attribute behavior and the removal of deprecated fields.
  • You will use HCL (HashiCorp Configuration Language) for Terraform and Python for pre-flight validation via the Genesys Cloud API.

Prerequisites

  • Terraform Version: 1.5.0 or later.
  • Genesys Cloud Terraform Provider: v1.35.0 or later.
  • Authentication: Service account with user:read and user:write scopes.
  • Python Environment: Python 3.9+ with requests library installed (pip install requests).
  • Understanding of the Breaking Change: In v1.35.0, the genesyscloud_user resource changed how the email attribute is handled. Previously, email was often derived from username or implicitly managed. Now, email is a distinct, required attribute for certain user types, and the username attribute behavior has been tightened to prevent conflicts with existing directory identities.

Authentication Setup

Before modifying Terraform state, you must authenticate to verify the current state of your users. This step ensures you identify which users require manual intervention before running terraform apply.

We will use Python to fetch user details using the Genesys Cloud REST API. This allows you to inspect the actual data stored in the platform, which may differ from what your Terraform state currently holds.

import requests
import json
import os
from typing import Dict, Any

class GenesysAuth:
    def __init__(self, client_id: str, client_secret: str, org_id: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.org_id = org_id
        self.base_url = f"https://{org_id}.mypurecloud.com/api/v2"
        self.token = None

    def authenticate(self) -> bool:
        """
        Authenticates using OAuth Client Credentials Grant.
        Returns True if successful, False otherwise.
        """
        auth_url = f"https://login.mypurecloud.com/oauth/token"
        
        payload = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "user:read user:write"
        }
        
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        
        try:
            response = requests.post(auth_url, data=payload, headers=headers)
            response.raise_for_status()
            self.token = response.json().get("access_token")
            return True
        except requests.exceptions.HTTPError as e:
            print(f"Authentication failed: {e.response.status_code} - {e.response.text}")
            return False
        except Exception as e:
            print(f"Unexpected error during authentication: {e}")
            return False

    def get_headers(self) -> Dict[str, str]:
        """Returns headers with Bearer token."""
        if not self.token:
            raise RuntimeError("Not authenticated. Call authenticate() first.")
        return {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }

# Initialize with environment variables
CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
ORG_ID = os.getenv("GENESYS_ORG_ID")

if not all([CLIENT_ID, CLIENT_SECRET, ORG_ID]):
    raise ValueError("Missing required environment variables: GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, GENESYS_ORG_ID")

auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, ORG_ID)
if not auth.authenticate():
    exit(1)

Implementation

Step 1: Identify Users Affected by Schema Changes

The breaking change in v1.35.0 primarily affects users where the email field was previously omitted or derived. In the new schema, if you do not explicitly define email, Terraform may attempt to recreate the resource or fail validation if the API expects a distinct email address.

First, use the Python script to list users and identify those where email might be missing or inconsistent with the username.

def list_users(auth: GenesysAuth, page_size: int = 25) -> list[Dict[str, Any]]:
    """
    Fetches all users using pagination.
    Returns a list of user dictionaries.
    """
    users = []
    uri = f"{auth.base_url}/users"
    headers = auth.get_headers()
    
    while uri:
        try:
            response = requests.get(uri, headers=headers)
            response.raise_for_status()
            data = response.json()
            
            users.extend(data.get("entities", []))
            
            # Check for next page
            if "nextPageUri" in data and data["nextPageUri"]:
                uri = data["nextPageUri"]
            else:
                uri = None
        except requests.exceptions.HTTPError as e:
            print(f"Error fetching users: {e.response.status_code} - {e.response.text}")
            break
            
    return users

# Execute fetch
users = list_users(auth)

# Identify problematic users
problematic_users = []
for user in users:
    username = user.get("username")
    email = user.get("email")
    
    # In v1.35.0, email is a critical identifier. 
    # If email is None or empty, or if it differs significantly from expected patterns,
    # the Terraform plan may show a destructive change.
    if not email or email == "":
        problematic_users.append({
            "id": user.get("id"),
            "name": user.get("name"),
            "username": username,
            "email": email,
            "issue": "Missing or empty email address"
        })
    elif username and email and username.lower() not in email.lower():
        # Check if username is not a subset of email (common pattern)
        problematic_users.append({
            "id": user.get("id"),
            "name": user.get("name"),
            "username": username,
            "email": email,
            "issue": "Username does not match email prefix"
        })

if problematic_users:
    print("Users requiring attention:")
    for u in problematic_users:
        print(json.dumps(u, indent=2))
else:
    print("No immediate schema conflicts detected in email fields.")

Step 2: Update Terraform Configuration for genesyscloud_user

The core of the migration involves updating your .tf files. In v1.35.0, the email attribute is now mandatory for new users and must be explicitly managed for existing ones to prevent Terraform from detecting a drift.

Before (v1.34.0 and earlier):

resource "genesyscloud_user" "example_user" {
  name     = "John Doe"
  email    = "john.doe@example.com"
  username = "john.doe"
  
  division {
    name = "Default"
  }
}

After (v1.35.0+):
While the structure looks similar, the validation logic has changed. You must ensure the email is unique and that the username matches the email prefix if your organization enforces this policy. More critically, if you were relying on implicit email generation, you must now specify it.

resource "genesyscloud_user" "example_user" {
  name     = "John Doe"
  email    = "john.doe@example.com" # Must be explicitly defined and unique
  username = "john.doe"             # Must match the email prefix if policy requires
  
  division {
    name = "Default"
  }

  # New in v1.35.0: Explicit handling of user types and roles
  user_types = ["AGENT"]
  
  # Ensure locale is set to prevent defaulting issues
  locale_settings {
    locale = "en-US"
    timezone = "America/Chicago"
  }
}

If you have many users, use a for_each loop to manage them efficiently. This approach also helps in debugging which specific user is causing the plan to fail.

locals {
  users = {
    "john.doe" = {
      name   = "John Doe"
      email  = "john.doe@example.com"
      type   = "AGENT"
    }
    "jane.smith" = {
      name   = "Jane Smith"
      email  = "jane.smith@example.com"
      type   = "SUPERVISOR"
    }
  }
}

resource "genesyscloud_user" "managed_users" {
  for_each = local.users
  
  name     = each.value.name
  email    = each.value.email
  username = split("@", each.value.email)[0] # Derive username from email for consistency
  
  division {
    name = "Default"
  }

  user_types = [each.value.type]

  locale_settings {
    locale = "en-US"
    timezone = "America/Chicago"
  }
}

Step 3: Validate and Apply with Dry-Run

Before applying changes, run terraform plan to identify any resources that Terraform intends to destroy and recreate. The breaking change in v1.35.0 may cause Terraform to see a mismatch in the email or username attributes if they were previously managed implicitly.

# Initialize the provider
terraform init -upgrade

# Plan to detect changes
terraform plan -out=tfplan

If the plan shows a recreation of users, inspect the diff. If the only change is in the email attribute representation, you may need to update the state file manually to reflect the new schema without triggering a recreation.

# Inspect the planned changes
terraform show tfplan

Complete Working Example

Below is a complete Python script that validates user data against the new schema requirements and outputs a corrected Terraform configuration snippet. This script assumes you have a list of users in a JSON file and wants to generate the corresponding HCL.

import json
import os
from typing import Dict, List, Any

def generate_terraform_config(users: List[Dict[str, Any]], output_file: str = "users.tf") -> None:
    """
    Generates a Terraform configuration file for a list of users.
    Ensures compliance with v1.35.0 schema requirements.
    """
    
    # Start the HCL content
    hcl_content = """locals {
  users = {
"""
    
    for user in users:
        username = user.get("username")
        email = user.get("email")
        name = user.get("name")
        user_type = user.get("user_type", "AGENT")
        
        if not username or not email:
            print(f"Skipping user {name}: missing username or email")
            continue
            
        # Ensure username matches email prefix for consistency
        email_prefix = email.split("@")[0]
        if username.lower() != email_prefix.lower():
            print(f"Warning: Username '{username}' does not match email prefix '{email_prefix}' for user {name}")
            # Optional: Force username to match email prefix
            username = email_prefix
            
        hcl_content += f"""    "{username}" = {{
      name   = "{name}"
      email  = "{email}"
      type   = "{user_type}"
    }}
"""
    
    hcl_content += """  }
}

resource "genesyscloud_user" "managed_users" {
  for_each = local.users

  name     = each.value.name
  email    = each.value.email
  username = split("@", each.value.email)[0]

  division {
    name = "Default"
  }

  user_types = [each.value.type]

  locale_settings {
    locale = "en-US"
    timezone = "America/Chicago"
  }
}
"""
    
    with open(output_file, "w") as f:
        f.write(hcl_content)
        
    print(f"Terraform configuration written to {output_file}")

# Example usage
if __name__ == "__main__":
    sample_users = [
        {
            "name": "John Doe",
            "email": "john.doe@example.com",
            "username": "john.doe",
            "user_type": "AGENT"
        },
        {
            "name": "Jane Smith",
            "email": "jane.smith@example.com",
            "username": "jane.smith",
            "user_type": "SUPERVISOR"
        }
    ]
    
    generate_terraform_config(sample_users)

Common Errors & Debugging

Error: Error creating user: 409 Conflict

What causes it:
This error occurs when you attempt to create a user with an email address or username that already exists in the Genesys Cloud organization. The v1.35.0 schema enforces stricter uniqueness constraints on the email field.

How to fix it:

  1. Verify that the email address in your Terraform configuration does not already exist in the platform.
  2. If the user exists but is not managed by Terraform, import it into the state.
  3. If the user is managed by Terraform but the email has changed, update the configuration and run terraform apply.
# Import an existing user into Terraform state
terraform import genesyscloud_user.managed_users["john.doe"] <user_id>

Error: Attribute "email" is required

What causes it:
In v1.35.0, the email attribute is mandatory. If your Terraform configuration omits this field, the provider will fail validation.

How to fix it:
Add the email attribute to your genesyscloud_user resource. Ensure it is a valid email address and unique within the organization.

resource "genesyscloud_user" "example" {
  name     = "John Doe"
  email    = "john.doe@example.com" # Add this line
  username = "john.doe"
  
  division {
    name = "Default"
  }
}

Error: Invalid value for "username"

What causes it:
The username attribute must be unique and conform to Genesys Cloud naming conventions. In v1.35.0, the provider may enforce that the username matches the prefix of the email address if your organization has such a policy.

How to fix it:
Ensure the username matches the email prefix. Use the split function in Terraform to derive the username from the email if necessary.

username = split("@", each.value.email)[0]

Official References