From afe0a6bd6670cef6f97c6619a6a797091f99977b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20H=C3=A4ring?= Date: Thu, 23 Jul 2026 08:04:34 +0200 Subject: [PATCH] fix: page through all list results instead of relying on Page: -1 Page: -1 does not return everything. The SDK sends page=0&limit=0 and Forgejo answers with just the default first page (~30 items), so listing and then searching by name or id missed anything past page 1. Walk the pages instead. Same as the team fix, applied to the remaining list-then-search data sources and the action secret resources. --- CHANGELOG.md | 6 + internal/provider/deploy_key_data_source.go | 111 +++++++------ internal/provider/gpg_key_data_source.go | 147 ++++++++++-------- .../organization_action_secret_resource.go | 110 +++++++------ internal/provider/organization_data_source.go | 98 +++++++----- .../repository_action_secret_resource.go | 116 +++++++------- internal/provider/ssh_key_data_source.go | 109 +++++++------ internal/provider/team_data_source.go | 103 ++++++------ 8 files changed, 454 insertions(+), 346 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3bd2b..499638c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +BUG FIXES: + +- All list-then-search lookups: Page through every result instead of relying on `Page: -1`, which only returns the first page + ## 1.5.1 (July 12, 2026) ENHANCEMENTS: diff --git a/internal/provider/deploy_key_data_source.go b/internal/provider/deploy_key_data_source.go index 2203f25..8c6f812 100644 --- a/internal/provider/deploy_key_data_source.go +++ b/internal/provider/deploy_key_data_source.go @@ -143,51 +143,68 @@ func (d *deployKeyDataSource) Read(ctx context.Context, req datasource.ReadReque "repo": repo.Name.ValueString(), }) - // Use Forgejo client to list deploy keys - keys, res, err := d.client.ListDeployKeys( - repo.Owner.ValueString(), - repo.Name.ValueString(), - forgejo.ListDeployKeysOptions{ - ListOptions: forgejo.ListOptions{ - Page: -1, + // Page through all deploy keys explicitly: ListOptions{Page: -1} only + // returns the server's default first page (~30), so a key past page 1 + // would be missed. + const pageSize = 50 + var key *forgejo.DeployKey + for page := 1; ; page++ { + keys, res, err := d.client.ListDeployKeys( + repo.Owner.ValueString(), + repo.Name.ValueString(), + forgejo.ListDeployKeysOptions{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) - } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) - - switch res.StatusCode { - case 404: - msg = fmt.Sprintf( - "Deploy keys with user %s and repo %s not found: %s", - repo.Owner.String(), - repo.Name.String(), - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + ) + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) + + switch res.StatusCode { + case 404: + msg = fmt.Sprintf( + "Deploy keys with user %s and repo %s not found: %s", + repo.Owner.String(), + repo.Name.String(), + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } + resp.Diagnostics.AddError("Unable to list deploy keys", msg) + + return } - resp.Diagnostics.AddError("Unable to list deploy keys", msg) - return - } + // Search this page for a deploy key with the given title. + idx := slices.IndexFunc(keys, func(k *forgejo.DeployKey) bool { + return k.Title == data.Title.ValueString() + }) + if idx != -1 { + key = keys[idx] + break + } - // Search for deploy key with given name - idx := slices.IndexFunc(keys, func(k *forgejo.DeployKey) bool { - return k.Title == data.Title.ValueString() - }) - if idx == -1 { + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(keys) == 0 { + break + } + } + if key == nil { resp.Diagnostics.AddError( "Unable to find deploy key by title", fmt.Sprintf( @@ -202,13 +219,13 @@ func (d *deployKeyDataSource) Read(ctx context.Context, req datasource.ReadReque } // Map response body to model - data.KeyID = types.Int64Value(keys[idx].ID) - data.Key = types.StringValue(keys[idx].Key) - data.URL = types.StringValue(keys[idx].URL) - data.Title = types.StringValue(keys[idx].Title) - data.Fingerprint = types.StringValue(keys[idx].Fingerprint) - data.Created = types.StringValue(keys[idx].Created.Format(time.RFC3339)) - data.ReadOnly = types.BoolValue(keys[idx].ReadOnly) + data.KeyID = types.Int64Value(key.ID) + data.Key = types.StringValue(key.Key) + data.URL = types.StringValue(key.URL) + data.Title = types.StringValue(key.Title) + data.Fingerprint = types.StringValue(key.Fingerprint) + data.Created = types.StringValue(key.Created.Format(time.RFC3339)) + data.ReadOnly = types.BoolValue(key.ReadOnly) // Save data into Terraform state diags = resp.State.Set(ctx, &data) diff --git a/internal/provider/gpg_key_data_source.go b/internal/provider/gpg_key_data_source.go index 82ed69b..66bbd35 100644 --- a/internal/provider/gpg_key_data_source.go +++ b/internal/provider/gpg_key_data_source.go @@ -153,66 +153,85 @@ func (d *gpgKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, "user": data.User.ValueString(), }) - var ( - keys []*forgejo.GPGKey - res *forgejo.Response - err error - ) - - // Use Forgejo client to list GPG keys - if data.User.ValueString() != "" { - keys, res, err = d.client.ListGPGKeys( - data.User.ValueString(), - forgejo.ListGPGKeysOptions{ - ListOptions: forgejo.ListOptions{ - Page: -1, - }, - }, + // Page through all GPG keys explicitly: ListOptions{Page: -1} only + // returns the server's default first page (~30), so a key past page 1 + // would be missed. + const pageSize = 50 + var key *forgejo.GPGKey + for page := 1; ; page++ { + var ( + keys []*forgejo.GPGKey + res *forgejo.Response + err error ) - } else { - keys, res, err = d.client.ListMyGPGKeys( - &forgejo.ListGPGKeysOptions{ - ListOptions: forgejo.ListOptions{ - Page: -1, + + // Use Forgejo client to list GPG keys + if data.User.ValueString() != "" { + keys, res, err = d.client.ListGPGKeys( + data.User.ValueString(), + forgejo.ListGPGKeysOptions{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - } - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) + ) } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) + keys, res, err = d.client.ListMyGPGKeys( + &forgejo.ListGPGKeysOptions{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, + }, + ) + } + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) - switch res.StatusCode { - case 404: - // If the user was not provided, we should never get a 404, so the message here should always have a user. - msg = fmt.Sprintf( - "GPG keys for user %s not found: %s", - data.User.String(), - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + switch res.StatusCode { + case 404: + // If the user was not provided, we should never get a 404, so the message here should always have a user. + msg = fmt.Sprintf( + "GPG keys for user %s not found: %s", + data.User.String(), + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } + resp.Diagnostics.AddError("Unable to list GPG keys", msg) + + return } - resp.Diagnostics.AddError("Unable to list GPG keys", msg) - return - } + // Search this page for a GPG key with the given ID. + idx := slices.IndexFunc(keys, func(k *forgejo.GPGKey) bool { + return strings.EqualFold(k.KeyID, data.KeyID.ValueString()) + }) + if idx != -1 { + key = keys[idx] + break + } - // Search for GPG key with given title - idx := slices.IndexFunc(keys, func(k *forgejo.GPGKey) bool { - return strings.EqualFold(k.KeyID, data.KeyID.ValueString()) - }) - if idx == -1 { + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(keys) == 0 { + break + } + } + if key == nil { var msg string if data.User.ValueString() != "" { msg = fmt.Sprintf( @@ -232,21 +251,21 @@ func (d *gpgKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, } // Map response body to model - data.ID = types.Int64Value(keys[idx].ID) - data.KeyID = types.StringValue(keys[idx].KeyID) - data.PrimaryKeyID = types.StringValue(keys[idx].PrimaryKeyID) - data.PublicKey = types.StringValue(keys[idx].PublicKey) - data.CanSign = types.BoolValue(keys[idx].CanSign) - data.CanEncryptComms = types.BoolValue(keys[idx].CanEncryptComms) - data.CanEncryptStorage = types.BoolValue(keys[idx].CanEncryptStorage) - data.CanCertify = types.BoolValue(keys[idx].CanCertify) - data.Created = types.StringValue(keys[idx].Created.Format(time.RFC3339)) - data.Expires = types.StringValue(keys[idx].Expires.Format(time.RFC3339)) + data.ID = types.Int64Value(key.ID) + data.KeyID = types.StringValue(key.KeyID) + data.PrimaryKeyID = types.StringValue(key.PrimaryKeyID) + data.PublicKey = types.StringValue(key.PublicKey) + data.CanSign = types.BoolValue(key.CanSign) + data.CanEncryptComms = types.BoolValue(key.CanEncryptComms) + data.CanEncryptStorage = types.BoolValue(key.CanEncryptStorage) + data.CanCertify = types.BoolValue(key.CanCertify) + data.Created = types.StringValue(key.Created.Format(time.RFC3339)) + data.Expires = types.StringValue(key.Expires.Format(time.RFC3339)) - data.Emails, diags = getEmails(keys[idx]) + data.Emails, diags = getEmails(key) resp.Diagnostics.Append(diags...) - data.Subkeys, diags = getSubkeys(keys[idx]) + data.Subkeys, diags = getSubkeys(key) resp.Diagnostics.Append(diags...) // Save data into Terraform state diff --git a/internal/provider/organization_action_secret_resource.go b/internal/provider/organization_action_secret_resource.go index 993d7a0..556dbb5 100644 --- a/internal/provider/organization_action_secret_resource.go +++ b/internal/provider/organization_action_secret_resource.go @@ -449,60 +449,72 @@ func (r *organizationActionSecretResource) getSecret(ctx context.Context, org, n "name": name, }) - // Use Forgejo client to list organization action secrets - secrets, res, err := r.client.ListOrgActionSecret( - org, - forgejo.ListOrgActionSecretOption{ - ListOptions: forgejo.ListOptions{ - Page: -1, + // Page through all secrets explicitly: ListOptions{Page: -1} only returns + // the server's default first page (~30), so a secret past page 1 would be + // missed and Read would fail with "not found" for a secret that exists. + const pageSize = 50 + for page := 1; ; page++ { + secrets, res, err := r.client.ListOrgActionSecret( + org, + forgejo.ListOrgActionSecretOption{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) - } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) - - switch res.StatusCode { - case 404: - msg = fmt.Sprintf( - "Action secrets with organization '%s' not found: %s", - org, - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + ) + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) + + switch res.StatusCode { + case 404: + msg = fmt.Sprintf( + "Action secrets with organization '%s' not found: %s", + org, + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } - } - diags.AddError("Unable to list organization action secrets", msg) + diags.AddError("Unable to list organization action secrets", msg) - return nil, diags - } + return nil, diags + } - // Search for organization action secrets with given name - idx := slices.IndexFunc(secrets, func(s *forgejo.Secret) bool { - return strings.EqualFold(s.Name, name) - }) - if idx == -1 { - diags.AddError( - "Unable to find organization action secret by name", - fmt.Sprintf( - "Action secret with organization '%s' and name '%s' not found", - org, - name, - ), - ) + // Search this page for an organization action secret with the given name. + idx := slices.IndexFunc(secrets, func(s *forgejo.Secret) bool { + return strings.EqualFold(s.Name, name) + }) + if idx != -1 { + return secrets[idx], diags + } - return nil, diags + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(secrets) == 0 { + break + } } - return secrets[idx], diags + diags.AddError( + "Unable to find organization action secret by name", + fmt.Sprintf( + "Action secret with organization '%s' and name '%s' not found", + org, + name, + ), + ) + + return nil, diags } diff --git a/internal/provider/organization_data_source.go b/internal/provider/organization_data_source.go index d7edfed..92f2f84 100644 --- a/internal/provider/organization_data_source.go +++ b/internal/provider/organization_data_source.go @@ -160,56 +160,68 @@ func getOrganizationByID(ctx context.Context, client *forgejo.Client, id int64) "id": id, }) - // Use Forgejo client to list organizations - orgs, res, err := client.ListMyOrgs( - forgejo.ListOrgsOptions{ - ListOptions: forgejo.ListOptions{ - Page: -1, + // Page through all organizations explicitly: ListOptions{Page: -1} only + // returns the server's default first page (~30), so an org past page 1 + // would be missed. + const pageSize = 50 + for page := 1; ; page++ { + orgs, res, err := client.ListMyOrgs( + forgejo.ListOrgsOptions{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) - } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) - - switch res.StatusCode { - case 403: - msg = fmt.Sprintf( - "Listing organizations forbidden: %s", - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + ) + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) + + switch res.StatusCode { + case 403: + msg = fmt.Sprintf( + "Listing organizations forbidden: %s", + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } - } - diags.AddError("Unable to list organizations", msg) + diags.AddError("Unable to list organizations", msg) - return nil, diags - } + return nil, diags + } - // Search for organization with given ID - idx := slices.IndexFunc(orgs, func(o *forgejo.Organization) bool { - return o.ID == id - }) - if idx == -1 { - diags.AddError( - "Unable to find organization by ID", - fmt.Sprintf("Organization with ID %d not found", id), - ) + // Search this page for an organization with the given ID. + idx := slices.IndexFunc(orgs, func(o *forgejo.Organization) bool { + return o.ID == id + }) + if idx != -1 { + return orgs[idx], diags + } - return nil, diags + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(orgs) == 0 { + break + } } - return orgs[idx], diags + diags.AddError( + "Unable to find organization by ID", + fmt.Sprintf("Organization with ID %d not found", id), + ) + + return nil, diags } // getOrganizationByName fetches an organization by its name and handles errors consistently. diff --git a/internal/provider/repository_action_secret_resource.go b/internal/provider/repository_action_secret_resource.go index 3ac2947..7560903 100644 --- a/internal/provider/repository_action_secret_resource.go +++ b/internal/provider/repository_action_secret_resource.go @@ -476,63 +476,75 @@ func (r *repositoryActionSecretResource) getSecret(ctx context.Context, owner, r "name": name, }) - // Use Forgejo client to list repository action secrets - secrets, res, err := r.client.ListRepoActionSecret( - owner, - repo, - forgejo.ListRepoActionSecretOption{ - ListOptions: forgejo.ListOptions{ - Page: -1, + // Page through all secrets explicitly: ListOptions{Page: -1} only returns + // the server's default first page (~30), so a secret past page 1 would be + // missed and Read would fail with "not found" for a secret that exists. + const pageSize = 50 + for page := 1; ; page++ { + secrets, res, err := r.client.ListRepoActionSecret( + owner, + repo, + forgejo.ListRepoActionSecretOption{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) - } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) - - switch res.StatusCode { - case 404: - msg = fmt.Sprintf( - "Action secrets with owner '%s' and repo '%s' not found: %s", - owner, - repo, - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + ) + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) + + switch res.StatusCode { + case 404: + msg = fmt.Sprintf( + "Action secrets with owner '%s' and repo '%s' not found: %s", + owner, + repo, + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } - } - diags.AddError("Unable to list repository action secrets", msg) + diags.AddError("Unable to list repository action secrets", msg) - return nil, diags - } + return nil, diags + } - // Search for repository action secrets with given name - idx := slices.IndexFunc(secrets, func(s *forgejo.Secret) bool { - return strings.EqualFold(s.Name, name) - }) - if idx == -1 { - diags.AddError( - "Unable to find repository action secret by name", - fmt.Sprintf( - "Action secret with owner '%s' repo '%s' and name '%s' not found", - owner, - repo, - name, - ), - ) + // Search this page for a repository action secret with the given name. + idx := slices.IndexFunc(secrets, func(s *forgejo.Secret) bool { + return strings.EqualFold(s.Name, name) + }) + if idx != -1 { + return secrets[idx], diags + } - return nil, diags + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(secrets) == 0 { + break + } } - return secrets[idx], diags + diags.AddError( + "Unable to find repository action secret by name", + fmt.Sprintf( + "Action secret with owner '%s' repo '%s' and name '%s' not found", + owner, + repo, + name, + ), + ) + + return nil, diags } diff --git a/internal/provider/ssh_key_data_source.go b/internal/provider/ssh_key_data_source.go index 9ae8ad6..f193aa0 100644 --- a/internal/provider/ssh_key_data_source.go +++ b/internal/provider/ssh_key_data_source.go @@ -130,49 +130,66 @@ func (d *sshKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, "user": data.User.ValueString(), }) - // Use Forgejo client to list SSH keys - keys, res, err := d.client.ListPublicKeys( - data.User.ValueString(), - forgejo.ListPublicKeysOptions{ - ListOptions: forgejo.ListOptions{ - Page: -1, + // Page through all SSH keys explicitly: ListOptions{Page: -1} only + // returns the server's default first page (~30), so a key past page 1 + // would be missed. + const pageSize = 50 + var key *forgejo.PublicKey + for page := 1; ; page++ { + keys, res, err := d.client.ListPublicKeys( + data.User.ValueString(), + forgejo.ListPublicKeysOptions{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) - } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) - - switch res.StatusCode { - case 404: - msg = fmt.Sprintf( - "SSH keys for user %s not found: %s", - data.User.String(), - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + ) + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) + + switch res.StatusCode { + case 404: + msg = fmt.Sprintf( + "SSH keys for user %s not found: %s", + data.User.String(), + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } + resp.Diagnostics.AddError("Unable to list SSH keys", msg) + + return } - resp.Diagnostics.AddError("Unable to list SSH keys", msg) - return - } + // Search this page for an SSH key with the given title. + idx := slices.IndexFunc(keys, func(k *forgejo.PublicKey) bool { + return k.Title == data.Title.ValueString() + }) + if idx != -1 { + key = keys[idx] + break + } - // Search for SSH key with given title - idx := slices.IndexFunc(keys, func(k *forgejo.PublicKey) bool { - return k.Title == data.Title.ValueString() - }) - if idx == -1 { + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(keys) == 0 { + break + } + } + if key == nil { resp.Diagnostics.AddError( "Unable to find SSH key by title", fmt.Sprintf( @@ -186,14 +203,14 @@ func (d *sshKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, } // Map response body to model - data.KeyID = types.Int64Value(keys[idx].ID) - data.Key = types.StringValue(keys[idx].Key) - data.URL = types.StringValue(keys[idx].URL) - data.Title = types.StringValue(keys[idx].Title) - data.Fingerprint = types.StringValue(keys[idx].Fingerprint) - data.Created = types.StringValue(keys[idx].Created.Format(time.RFC3339)) - data.ReadOnly = types.BoolValue(keys[idx].ReadOnly) - data.KeyType = types.StringValue(keys[idx].KeyType) + data.KeyID = types.Int64Value(key.ID) + data.Key = types.StringValue(key.Key) + data.URL = types.StringValue(key.URL) + data.Title = types.StringValue(key.Title) + data.Fingerprint = types.StringValue(key.Fingerprint) + data.Created = types.StringValue(key.Created.Format(time.RFC3339)) + data.ReadOnly = types.BoolValue(key.ReadOnly) + data.KeyType = types.StringValue(key.KeyType) // Save data into Terraform state diags = resp.State.Set(ctx, &data) diff --git a/internal/provider/team_data_source.go b/internal/provider/team_data_source.go index e93ec52..fd4f860 100644 --- a/internal/provider/team_data_source.go +++ b/internal/provider/team_data_source.go @@ -255,56 +255,69 @@ func getOrgTeamByName(ctx context.Context, client *forgejo.Client, org, name str "name": name, }) - // Use Forgejo client to list teams in organization - teams, res, err := client.ListOrgTeams( - org, - forgejo.ListTeamsOptions{ - ListOptions: forgejo.ListOptions{ - Page: -1, + // Page through all teams in the organization. ListOptions{Page: -1} does + // NOT return every team - it sends the server's default first page (30 + // teams), so an org with more teams than that would never find a team past + // the first page. Walk the pages explicitly instead. + const pageSize = 50 + for page := 1; ; page++ { + teams, res, err := client.ListOrgTeams( + org, + forgejo.ListTeamsOptions{ + ListOptions: forgejo.ListOptions{ + Page: page, + PageSize: pageSize, + }, }, - }, - ) - if err != nil { - var msg string - if res == nil { - msg = fmt.Sprintf("Unknown error with nil response: %s", err) - } else { - tflog.Error(ctx, "Error", map[string]any{ - "status": res.Status, - }) - - switch res.StatusCode { - case 404: - msg = fmt.Sprintf( - "Organization with name '%s' not found: %s", - org, - err, - ) - default: - msg = fmt.Sprintf( - "Unknown error (status %d): %s", - res.StatusCode, - err, - ) + ) + if err != nil { + var msg string + if res == nil { + msg = fmt.Sprintf("Unknown error with nil response: %s", err) + } else { + tflog.Error(ctx, "Error", map[string]any{ + "status": res.Status, + }) + + switch res.StatusCode { + case 404: + msg = fmt.Sprintf( + "Organization with name '%s' not found: %s", + org, + err, + ) + default: + msg = fmt.Sprintf( + "Unknown error (status %d): %s", + res.StatusCode, + err, + ) + } } - } - diags.AddError("Unable to list teams", msg) + diags.AddError("Unable to list teams", msg) - return nil, diags - } + return nil, diags + } - // Search for team with given name - idx := slices.IndexFunc(teams, func(t *forgejo.Team) bool { - return t.Name == name - }) - if idx == -1 { - diags.AddError( - "Unable to find team by name", - fmt.Sprintf("Team with name '%s' not found", name), - ) + // Search this page for a team with the given name. + idx := slices.IndexFunc(teams, func(t *forgejo.Team) bool { + return t.Name == name + }) + if idx != -1 { + return teams[idx], diags + } - return nil, diags + // Only an empty page proves this was the last page. The server caps + // the page size at MAX_RESPONSE_ITEMS, so a short page is no proof. + if len(teams) == 0 { + break + } } - return teams[idx], diags + diags.AddError( + "Unable to find team by name", + fmt.Sprintf("Team with name '%s' not found", name), + ) + + return nil, diags }