Skip to content

Commit b5708e6

Browse files
committed
Merge remote-tracking branch 'origin'
2 parents 6c68e1d + 4580306 commit b5708e6

File tree

4 files changed

+205
-108
lines changed

4 files changed

+205
-108
lines changed

internal/client/client.go

Lines changed: 18 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,19 +1856,33 @@ func (c *OpenAIClient) AddProjectUser(projectID, userID, role string) (*ProjectU
18561856
//
18571857
// Parameters:
18581858
// - projectID: The ID of the project to list users from
1859+
// - after: Cursor for pagination (empty string for first page)
1860+
// - limit: Maximum number of users to return per page
18591861
//
18601862
// Returns:
1861-
// - A ProjectUserList object with all users in the project
1863+
// - A ProjectUserList object with users in the project
18621864
// - An error if the operation failed
1863-
func (c *OpenAIClient) ListProjectUsers(projectID string) (*ProjectUserList, error) {
1865+
func (c *OpenAIClient) ListProjectUsers(projectID, after string, limit int) (*ProjectUserList, error) {
1866+
// Build query parameters
1867+
queryParams := url.Values{}
1868+
if after != "" {
1869+
queryParams.Add("after", after)
1870+
}
1871+
if limit > 0 {
1872+
queryParams.Add("limit", fmt.Sprintf("%d", limit))
1873+
}
1874+
18641875
// Construct the URL for the request
1865-
url := fmt.Sprintf("/v1/organization/projects/%s/users", projectID)
1876+
urlPath := fmt.Sprintf("/v1/organization/projects/%s/users", projectID)
1877+
if len(queryParams) > 0 {
1878+
urlPath = urlPath + "?" + queryParams.Encode()
1879+
}
18661880

18671881
// Log the request for debugging
18681882
fmt.Printf("[DEBUG] Listing users for project %s\n", projectID)
18691883

18701884
// Make the request
1871-
respBody, err := c.doRequest(http.MethodGet, url, nil)
1885+
respBody, err := c.doRequest(http.MethodGet, urlPath, nil)
18721886
if err != nil {
18731887
return nil, err
18741888
}
@@ -1882,64 +1896,6 @@ func (c *OpenAIClient) ListProjectUsers(projectID string) (*ProjectUserList, err
18821896
return &userList, nil
18831897
}
18841898

1885-
// FindProjectUser checks if a user exists in a project by ID.
1886-
// This is a helper method to avoid having to iterate through users in the provider.
1887-
//
1888-
// Parameters:
1889-
// - projectID: The ID of the project to check
1890-
// - userID: The ID of the user to find
1891-
//
1892-
// Returns:
1893-
// - The found ProjectUser if it exists
1894-
// - A boolean indicating if the user was found
1895-
// - An error if the operation failed
1896-
func (c *OpenAIClient) FindProjectUser(projectID, userID string) (*ProjectUser, bool, error) {
1897-
// Get all users in the project
1898-
userList, err := c.ListProjectUsers(projectID)
1899-
if err != nil {
1900-
return nil, false, err
1901-
}
1902-
1903-
// Look for the user in the list
1904-
for _, user := range userList.Data {
1905-
if user.ID == userID {
1906-
return &user, true, nil
1907-
}
1908-
}
1909-
1910-
// User not found
1911-
return nil, false, nil
1912-
}
1913-
1914-
// FindProjectUserByEmail checks if a user exists in a project by email address.
1915-
// This is a helper method to allow looking up users by email instead of ID.
1916-
//
1917-
// Parameters:
1918-
// - projectID: The ID of the project to check
1919-
// - email: The email address of the user to find
1920-
//
1921-
// Returns:
1922-
// - The found ProjectUser if it exists
1923-
// - A boolean indicating if the user was found
1924-
// - An error if the operation failed
1925-
func (c *OpenAIClient) FindProjectUserByEmail(projectID, email string) (*ProjectUser, bool, error) {
1926-
// Get all users in the project
1927-
userList, err := c.ListProjectUsers(projectID)
1928-
if err != nil {
1929-
return nil, false, err
1930-
}
1931-
1932-
// Look for the user with matching email in the list (case insensitive)
1933-
for _, user := range userList.Data {
1934-
if strings.EqualFold(user.Email, email) {
1935-
return &user, true, nil
1936-
}
1937-
}
1938-
1939-
// User not found
1940-
return nil, false, nil
1941-
}
1942-
19431899
// RemoveProjectUser removes a user from a project.
19441900
// Users who are organization owners cannot be removed from projects.
19451901
//

internal/provider/data_source_openai_project_user.go

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package provider
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/hashicorp/terraform-plugin-log/tflog"
89
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -47,21 +48,103 @@ func dataSourceOpenAIProjectUser() *schema.Resource {
4748
}
4849
}
4950

50-
// dataSourceOpenAIProjectUserRead handles the read operation for the OpenAI project user data source.
51-
// It retrieves information about a specific user in a project from the OpenAI API.
52-
func dataSourceOpenAIProjectUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
53-
c, err := GetOpenAIClientWithAdminKey(m)
51+
// dataSourceFindProjectUser finds a user in a project by ID with automatic pagination.
52+
// This is used by the data source to ensure proper user lookups across all pages.
53+
func dataSourceFindProjectUser(ctx context.Context, c interface{}, projectID, userID string) (*client.ProjectUser, bool, error) {
54+
clientInstance, err := GetOpenAIClientWithAdminKey(c)
5455
if err != nil {
55-
return diag.FromErr(err)
56+
return nil, false, err
57+
}
58+
59+
const batchSize = 100
60+
tflog.Debug(ctx, fmt.Sprintf("Finding user %s in project %s with pagination", userID, projectID))
61+
62+
var after string
63+
hasMore := true
64+
pageCount := 0
65+
66+
for hasMore {
67+
pageCount++
68+
tflog.Debug(ctx, fmt.Sprintf("Fetching page %d for project %s (after: %s)", pageCount, projectID, after))
69+
70+
userList, err := clientInstance.ListProjectUsers(projectID, after, batchSize)
71+
if err != nil {
72+
return nil, false, fmt.Errorf("error fetching project users (page %d): %w", pageCount, err)
73+
}
74+
75+
// Look for the user in this page
76+
for _, user := range userList.Data {
77+
if user.ID == userID {
78+
tflog.Debug(ctx, fmt.Sprintf("Found user %s in project %s on page %d", userID, projectID, pageCount))
79+
return &user, true, nil
80+
}
81+
}
82+
83+
// Check if there are more pages
84+
hasMore = userList.HasMore
85+
if hasMore && userList.LastID != "" {
86+
after = userList.LastID
87+
}
5688
}
5789

90+
tflog.Debug(ctx, fmt.Sprintf("User %s not found in project %s after checking %d pages", userID, projectID, pageCount))
91+
return nil, false, nil
92+
}
93+
94+
// dataSourceFindProjectUserByEmail finds a user in a project by email with automatic pagination.
95+
// This is used by the data source to ensure proper user lookups across all pages.
96+
func dataSourceFindProjectUserByEmail(ctx context.Context, c interface{}, projectID, email string) (*client.ProjectUser, bool, error) {
97+
clientInstance, err := GetOpenAIClientWithAdminKey(c)
98+
if err != nil {
99+
return nil, false, err
100+
}
101+
102+
const batchSize = 100
103+
tflog.Debug(ctx, fmt.Sprintf("Finding user by email %s in project %s with pagination", email, projectID))
104+
105+
var after string
106+
hasMore := true
107+
pageCount := 0
108+
109+
for hasMore {
110+
pageCount++
111+
tflog.Debug(ctx, fmt.Sprintf("Fetching page %d for project %s (after: %s)", pageCount, projectID, after))
112+
113+
userList, err := clientInstance.ListProjectUsers(projectID, after, batchSize)
114+
if err != nil {
115+
return nil, false, fmt.Errorf("error fetching project users (page %d): %w", pageCount, err)
116+
}
117+
118+
// Look for the user with matching email in this page (case insensitive)
119+
for _, user := range userList.Data {
120+
if strings.EqualFold(user.Email, email) {
121+
tflog.Debug(ctx, fmt.Sprintf("Found user with email %s in project %s on page %d", email, projectID, pageCount))
122+
return &user, true, nil
123+
}
124+
}
125+
126+
// Check if there are more pages
127+
hasMore = userList.HasMore
128+
if hasMore && userList.LastID != "" {
129+
after = userList.LastID
130+
}
131+
}
132+
133+
tflog.Debug(ctx, fmt.Sprintf("User with email %s not found in project %s after checking %d pages", email, projectID, pageCount))
134+
return nil, false, nil
135+
}
136+
137+
// dataSourceOpenAIProjectUserRead handles the read operation for the OpenAI project user data source.
138+
// It retrieves information about a specific user in a project from the OpenAI API.
139+
func dataSourceOpenAIProjectUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
58140
projectID := d.Get("project_id").(string)
59141
if projectID == "" {
60142
return diag.FromErr(fmt.Errorf("project_id is required"))
61143
}
62144

63145
var projectUser *client.ProjectUser
64146
var exists bool
147+
var err error
65148

66149
// Check if we're looking up by user_id or email
67150
if userID, ok := d.GetOk("user_id"); ok {
@@ -74,7 +157,7 @@ func dataSourceOpenAIProjectUserRead(ctx context.Context, d *schema.ResourceData
74157
tflog.Debug(ctx, fmt.Sprintf("Checking if user %s exists in project %s", userID, projectID))
75158

76159
// Check if the user exists in the project using the provider's API key
77-
projectUser, exists, err = c.FindProjectUser(projectID, userID)
160+
projectUser, exists, err = dataSourceFindProjectUser(ctx, m, projectID, userID)
78161
if err != nil {
79162
tflog.Error(ctx, fmt.Sprintf("Error checking if user exists: %v", err))
80163
return diag.Errorf("Error checking if user exists in project: %s", err)
@@ -96,7 +179,7 @@ func dataSourceOpenAIProjectUserRead(ctx context.Context, d *schema.ResourceData
96179
tflog.Debug(ctx, fmt.Sprintf("Checking if user with email %s exists in project %s", email, projectID))
97180

98181
// Check if the user exists in the project by email using the provider's API key
99-
projectUser, exists, err = c.FindProjectUserByEmail(projectID, email)
182+
projectUser, exists, err = dataSourceFindProjectUserByEmail(ctx, m, projectID, email)
100183
if err != nil {
101184
tflog.Error(ctx, fmt.Sprintf("Error checking if user exists by email: %v", err))
102185
return diag.Errorf("Error checking if user exists in project by email: %s", err)

internal/provider/data_source_openai_project_users.go

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -98,53 +98,77 @@ func dataSourceOpenAIProjectUsersRead(ctx context.Context, d *schema.ResourceDat
9898
// Set the ID to the project_id
9999
d.SetId(projectID)
100100

101-
// List all users in the project
102-
tflog.Debug(ctx, fmt.Sprintf("Listing users in project %s", projectID))
103-
usersList, err := c.ListProjectUsers(projectID)
104-
if err != nil {
105-
tflog.Error(ctx, fmt.Sprintf("Error listing users: %v", err))
106-
return diag.Errorf("Error listing users in project: %s", err)
107-
}
101+
// Automatic pagination - fetch all users with default batch size
102+
const batchSize = 100
103+
tflog.Debug(ctx, fmt.Sprintf("Fetching all users in project %s with batch size: %d", projectID, batchSize))
104+
105+
var allUsers []map[string]interface{}
106+
var after string
107+
hasMore := true
108+
pageCount := 0
109+
110+
// Paginate through all results
111+
for hasMore {
112+
pageCount++
113+
tflog.Debug(ctx, fmt.Sprintf("Fetching page %d for project %s (after: %s)", pageCount, projectID, after))
114+
115+
usersList, err := c.ListProjectUsers(projectID, after, batchSize)
116+
if err != nil {
117+
tflog.Error(ctx, fmt.Sprintf("Error listing users (page %d): %v", pageCount, err))
118+
return diag.Errorf("Error listing users in project (page %d): %s", pageCount, err)
119+
}
108120

109-
// Transform the users into a format appropriate for the schema
110-
users := make([]map[string]interface{}, 0, len(usersList.Data))
111-
for _, user := range usersList.Data {
112-
userData := map[string]interface{}{
113-
"id": user.ID,
114-
"email": user.Email,
115-
"role": user.Role,
116-
"added_at": user.AddedAt,
121+
// Transform the users from this page into a format appropriate for the schema
122+
for _, user := range usersList.Data {
123+
userData := map[string]interface{}{
124+
"id": user.ID,
125+
"email": user.Email,
126+
"role": user.Role,
127+
"added_at": user.AddedAt,
128+
}
129+
allUsers = append(allUsers, userData)
130+
}
131+
132+
// Check if there are more pages
133+
hasMore = usersList.HasMore
134+
if hasMore && usersList.LastID != "" {
135+
after = usersList.LastID
117136
}
118-
users = append(users, userData)
119137
}
120138

139+
tflog.Debug(ctx, fmt.Sprintf("Fetched %d total users for project %s across %d pages", len(allUsers), projectID, pageCount))
140+
141+
// Use allUsers instead of users
142+
users := allUsers
143+
121144
if err := d.Set("users", users); err != nil {
122145
return diag.FromErr(fmt.Errorf("error setting users: %s", err))
123146
}
124147

125148
// Extract user IDs
126-
userIDs := make([]string, 0, len(usersList.Data))
127-
for _, user := range usersList.Data {
128-
userIDs = append(userIDs, user.ID)
149+
userIDs := make([]string, 0, len(users))
150+
for _, user := range users {
151+
userIDs = append(userIDs, user["id"].(string))
129152
}
130153

131154
if err := d.Set("user_ids", userIDs); err != nil {
132155
return diag.FromErr(fmt.Errorf("error setting user_ids: %s", err))
133156
}
134157

135158
// Set user count for easy access
136-
if err := d.Set("user_count", len(usersList.Data)); err != nil {
159+
if err := d.Set("user_count", len(users)); err != nil {
137160
return diag.FromErr(fmt.Errorf("error setting user_count: %s", err))
138161
}
139162

140163
// Extract owner and member IDs
141164
ownerIDs := make([]string, 0)
142165
memberIDs := make([]string, 0)
143-
for _, user := range usersList.Data {
144-
if user.Role == "owner" {
145-
ownerIDs = append(ownerIDs, user.ID)
146-
} else if user.Role == "member" {
147-
memberIDs = append(memberIDs, user.ID)
166+
for _, user := range users {
167+
role := user["role"].(string)
168+
if role == "owner" {
169+
ownerIDs = append(ownerIDs, user["id"].(string))
170+
} else if role == "member" {
171+
memberIDs = append(memberIDs, user["id"].(string))
148172
}
149173
}
150174

0 commit comments

Comments
 (0)