Skip to content
32 changes: 32 additions & 0 deletions docs/data-sources/gpg_key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "forgejo_gpg_key Data Source - forgejo"
subcategory: ""
description: |-
Forgejo user GPG key data source.
---

# forgejo_gpg_key (Data Source)

Forgejo user GPG key data source.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `key_id` (String) ID of the GPG key.

### Read-Only

- `can_certify` (Boolean) Can this key certify.
- `can_encrypt_comms` (Boolean) Can this key encrypt communications.
- `can_encrypt_storage` (Boolean) Can this key encrypt storage.
- `can_sign` (Boolean) Can this key sign.
- `created_at` (String) Time at which the GPG key was created.
- `expires_at` (String) Time at which the GPG key expires.
- `fingerprint` (String) Fingerprint of the GPG key.
- `id` (Number) Numeric identifier of the GPG key.
- `primary_key_id` (String) Primary ID of the GPG key.
35 changes: 35 additions & 0 deletions docs/resources/gpg_key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "forgejo_gpg_key Resource - forgejo"
subcategory: ""
description: |-
Forgejo user GPG key resource.
Note: Managing user GPG keys requires administrative privileges!
---

# forgejo_gpg_key (Resource)

Forgejo user GPG key resource.
Note: Managing user GPG keys requires administrative privileges!



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `armored_public_key` (String) Armored GPG Public key.

### Read-Only

- `can_certify` (Boolean) Can this key certify.
- `can_encrypt_comms` (Boolean) Can this key encrypt communications.
- `can_encrypt_storage` (Boolean) Can this key encrypt storage.
- `can_sign` (Boolean) Can this key sign.
- `created_at` (String) Time at which the GPG key was created.
- `expires_at` (String) Time at which the GPG key expires.
- `fingerprint` (String) Fingerprint of the GPG key.
- `id` (Number) Numeric identifier of the GPG key.
- `key_id` (String) ID of the GPG key.
- `primary_key_id` (String) Primary ID of the GPG key.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ toolchain go1.24.10

require (
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0
github.com/ProtonMail/gopenpgp/v3 v3.3.0
github.com/hashicorp/terraform-plugin-framework v1.17.0
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
github.com/hashicorp/terraform-plugin-go v0.29.0
Expand All @@ -15,7 +16,7 @@ require (

require (
github.com/42wim/httpsig v1.2.3 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/gopenpgp/v3 v3.3.0 h1:N6rHCH5PWwB6zSRMgRj1EbAMQHUAAHxH3Oo4KibsPwY=
github.com/ProtonMail/gopenpgp/v3 v3.3.0/go.mod h1:J+iNPt0/5EO9wRt7Eit9dRUlzyu3hiGX3zId6iuaKOk=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
Expand Down
196 changes: 196 additions & 0 deletions internal/provider/gpg_key_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package provider

import (
"context"
"fmt"
"slices"
"strings"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"

"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)

// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &gpgKeyDataSource{}
_ datasource.DataSourceWithConfigure = &gpgKeyDataSource{}
)

// gpgKeyDataSource is the data source implementation.
type gpgKeyDataSource struct {
client *forgejo.Client
}

// gpgKeyDataSourceModel maps the data source schema data.
// https://pkg.go.dev/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2#GPGKey
type gpgKeyDataSourceModel struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Forgejo API also has Emails and SubsKey... did you omit them deliberately?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to keep the resource simple, based on the (possibly wrong) assumption that most users will just want to use this new resource to save/update their gpg key in forgejo.

I can add them if you think there's a use case for it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are absolutely right™ ;-)

I totally agree that we should only add code if there's a realistic use case for it. However, as the Forgejo SDK exposes these additional fields, I think that adding the two attributes comes at virtually no cost, and very little risk. Hence, I would probably add all the argumets that the Forgejo SDK offers, to give Terraform users access to them should the need arise...

But I also agree with your assessment - I trust your judgment and leave it up to you!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finally took the time to add them. It wasn't that hard, so might as well have everything.

ID types.Int64 `tfsdk:"id"`
KeyID types.String `tfsdk:"key_id"`
PrimaryKeyID types.String `tfsdk:"primary_key_id"`
Fingerprint types.String `tfsdk:"fingerprint"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called PublicKey in the Forgejo API... is Fingerprint more accurate?

@mathieu-lemay mathieu-lemay Jan 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* me doing my best "AI" Agent impression *: You're absolutely right!

I got confused by the fact that what I got back from the API call, and wrongly assumed it was a fingerprint. It was actually the public key itself, the reason it was small is that it was a ed25519 key.

CanSign types.Bool `tfsdk:"can_sign"`
CanEncryptComms types.Bool `tfsdk:"can_encrypt_comms"`
CanEncryptStorage types.Bool `tfsdk:"can_encrypt_storage"`
CanCertify types.Bool `tfsdk:"can_certify"`
Created types.String `tfsdk:"created_at"`
Expires types.String `tfsdk:"expires_at"`
}

// Metadata returns the data source type name.
func (d *gpgKeyDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_gpg_key"
}

// Schema defines the schema for the data source.
func (d *gpgKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Forgejo user GPG key data source.",

Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Description: "Numeric identifier of the GPG key.",
Computed: true,
},
"key_id": schema.StringAttribute{
Description: "ID of the GPG key.",
Required: true,
},
"primary_key_id": schema.StringAttribute{
Description: "Primary ID of the GPG key.",
Computed: true,
},
"fingerprint": schema.StringAttribute{
Description: "Fingerprint of the GPG key.",
Computed: true,
},
"can_sign": schema.BoolAttribute{
Description: "Can this key sign.",
Computed: true,
},
"can_encrypt_comms": schema.BoolAttribute{
Description: "Can this key encrypt communications.",
Computed: true,
},
"can_encrypt_storage": schema.BoolAttribute{
Description: "Can this key encrypt storage.",
Computed: true,
},
"can_certify": schema.BoolAttribute{
Description: "Can this key certify.",
Computed: true,
},
"created_at": schema.StringAttribute{
Description: "Time at which the GPG key was created.",
Computed: true,
},
"expires_at": schema.StringAttribute{
Description: "Time at which the GPG key expires.",
Computed: true,
},
},
}
}

// Configure adds the provider configured client to the data source.
func (d *gpgKeyDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*forgejo.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf(
"Expected *forgejo.Client, got: %T. Please report this issue to the provider developers.",
req.ProviderData,
),
)

return
}

d.client = client
}

// Read refreshes the Terraform state with the latest data.
func (d *gpgKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
defer un(trace(ctx, "Read GPG key data source"))

var data gpgKeyDataSourceModel

// Read Terraform configuration data into model
diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

tflog.Info(ctx, "List GPG keys")

// Use Forgejo client to list GPG keys
keys, res, err := d.client.ListMyGPGKeys(
&forgejo.ListGPGKeysOptions{},
)
if err != nil {
tflog.Error(ctx, "Error", map[string]any{
"status": res.Status,
})

var msg string
switch res.StatusCode {
case 404:
msg = fmt.Sprintf(
"GPG keys not found: %s",
err,
)
default:
msg = fmt.Sprintf("Unknown error: %s", err)
}
resp.Diagnostics.AddError("Unable to list GPG keys", msg)

return
}

// 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 {
resp.Diagnostics.AddError(
"Unable to get GPG key by key_id",
fmt.Sprintf(
"GPG key with key_id %s not found.",
data.KeyID.String(),
),
)

return
}

// 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.Fingerprint = 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.String())
data.Expires = types.StringValue(keys[idx].Expires.String())

// Save data into Terraform state
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}

// NewGPGKeyDataSource is a helper function to simplify the provider implementation.
func NewGPGKeyDataSource() datasource.DataSource {
return &gpgKeyDataSource{}
}
56 changes: 56 additions & 0 deletions internal/provider/gpg_key_data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package provider_test

import (
"fmt"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

func TestAccGPGKeyDataSource(t *testing.T) {
key, armoredPubKey := createGPGKey(t)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Read testing (non-existent resource)
{
Config: providerConfig + `
data "forgejo_gpg_key" "test" {
key_id= "non_existent"
}`,
ExpectError: regexp.MustCompile("GPG key with key_id \"non_existent\" not found"),
},
// Read testing
{
Config: providerConfig + fmt.Sprintf(`
resource "forgejo_gpg_key" "test" {
armored_public_key = <<EOT
%s
EOT
}
data "forgejo_gpg_key" "test" {
key_id = forgejo_gpg_key.test.key_id
}`, armoredPubKey),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("id"), knownvalue.NotNull()),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("key_id"), knownvalue.StringExact(strings.ToUpper(key.GetHexKeyID()))),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("primary_key_id"), knownvalue.StringExact("")),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("fingerprint"), knownvalue.NotNull()),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("can_sign"), knownvalue.Bool(true)),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("can_encrypt_comms"), knownvalue.Bool(false)),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("can_encrypt_storage"), knownvalue.Bool(false)),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("can_certify"), knownvalue.Bool(true)),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("created_at"), knownvalue.NotNull()),
statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("expires_at"), knownvalue.NotNull()),
},
},
},
})
}
Loading