Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
111 changes: 64 additions & 47 deletions internal/provider/deploy_key_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
147 changes: 83 additions & 64 deletions internal/provider/gpg_key_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
Loading