diff --git a/docs/data-sources/gpg_key.md b/docs/data-sources/gpg_key.md
new file mode 100644
index 0000000..2e7ae33
--- /dev/null
+++ b/docs/data-sources/gpg_key.md
@@ -0,0 +1,83 @@
+---
+# 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.
+
+## Example Usage
+
+```terraform
+terraform {
+ required_providers {
+ forgejo = {
+ source = "svalabs/forgejo"
+ }
+ }
+}
+
+provider "forgejo" {
+ host = "http://localhost:3000"
+}
+
+# Existing GPG key
+data "forgejo_gpg_key" "this" {
+ user = "test_user" # Optional, uses authenticated user if not provided
+ key_id = "test_key"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `key_id` (String) ID of the GPG key.
+
+### Optional
+
+- `user` (String) Name of the user.
+
+### 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.
+- `emails` (List of Object) Emails associated with the GPG key. (see [below for nested schema](#nestedatt--emails))
+- `expires_at` (String) Time at which the GPG key expires.
+- `id` (Number) Numeric identifier of the GPG key.
+- `primary_key_id` (String) Primary ID of the GPG key.
+- `public_key` (String) The public key.
+- `subkeys` (List of Object) Subkeys of the GPG key. (see [below for nested schema](#nestedatt--subkeys))
+
+
+### Nested Schema for `emails`
+
+Read-Only:
+
+- `email` (String)
+- `verified` (Boolean)
+
+
+
+### Nested Schema for `subkeys`
+
+Read-Only:
+
+- `can_certify` (Boolean)
+- `can_encrypt_comms` (Boolean)
+- `can_encrypt_storage` (Boolean)
+- `can_sign` (Boolean)
+- `created_at` (String)
+- `expires_at` (String)
+- `id` (Number)
+- `key_id` (String)
+- `primary_key_id` (String)
+- `public_key` (String)
diff --git a/docs/resources/gpg_key.md b/docs/resources/gpg_key.md
new file mode 100644
index 0000000..48c50d4
--- /dev/null
+++ b/docs/resources/gpg_key.md
@@ -0,0 +1,94 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "forgejo_gpg_key Resource - forgejo"
+subcategory: ""
+description: |-
+ Forgejo user GPG key resource.
+---
+
+# forgejo_gpg_key (Resource)
+
+Forgejo user GPG key resource.
+
+## Example Usage
+
+```terraform
+terraform {
+ required_providers {
+ forgejo = {
+ source = "svalabs/forgejo"
+ }
+ gpg = {
+ source = "terraform-provider-gpg/gpg"
+ }
+ }
+}
+
+variable "gpg_key_password" { sensitive = true }
+
+provider "forgejo" {
+ host = "http://localhost:3000"
+}
+
+# GPG key
+resource "gpg_key_pair" "test" {
+ identities = [{
+ name = "Test User"
+ email = "test_user@localhost.localdomain"
+ }]
+ passphrase = var.gpg_key_password
+}
+
+# Forgejo GPG key
+resource "forgejo_gpg_key" "this" {
+ # There is no user for this resource, GPG keys can only be managed for the authenticated user.
+ armored_public_key = gpg_key_pair.test.public_key
+}
+```
+
+
+## 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.
+- `emails` (List of Object) Emails associated with the GPG key. (see [below for nested schema](#nestedatt--emails))
+- `expires_at` (String) Time at which the GPG key expires.
+- `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.
+- `public_key` (String) The public key.
+- `subkeys` (List of Object) Subkeys of the GPG key. (see [below for nested schema](#nestedatt--subkeys))
+
+
+### Nested Schema for `emails`
+
+Read-Only:
+
+- `email` (String)
+- `verified` (Boolean)
+
+
+
+### Nested Schema for `subkeys`
+
+Read-Only:
+
+- `can_certify` (Boolean)
+- `can_encrypt_comms` (Boolean)
+- `can_encrypt_storage` (Boolean)
+- `can_sign` (Boolean)
+- `created_at` (String)
+- `expires_at` (String)
+- `id` (Number)
+- `key_id` (String)
+- `primary_key_id` (String)
+- `public_key` (String)
diff --git a/examples/data-sources/forgejo_gpg_key/data-source.tf b/examples/data-sources/forgejo_gpg_key/data-source.tf
new file mode 100644
index 0000000..1a83372
--- /dev/null
+++ b/examples/data-sources/forgejo_gpg_key/data-source.tf
@@ -0,0 +1,17 @@
+terraform {
+ required_providers {
+ forgejo = {
+ source = "svalabs/forgejo"
+ }
+ }
+}
+
+provider "forgejo" {
+ host = "http://localhost:3000"
+}
+
+# Existing GPG key
+data "forgejo_gpg_key" "this" {
+ user = "test_user" # Optional, uses authenticated user if not provided
+ key_id = "test_key"
+}
diff --git a/examples/resources/forgejo_gpg_key/resource.tf b/examples/resources/forgejo_gpg_key/resource.tf
new file mode 100644
index 0000000..d496bb6
--- /dev/null
+++ b/examples/resources/forgejo_gpg_key/resource.tf
@@ -0,0 +1,31 @@
+terraform {
+ required_providers {
+ forgejo = {
+ source = "svalabs/forgejo"
+ }
+ gpg = {
+ source = "terraform-provider-gpg/gpg"
+ }
+ }
+}
+
+variable "gpg_key_password" { sensitive = true }
+
+provider "forgejo" {
+ host = "http://localhost:3000"
+}
+
+# GPG key
+resource "gpg_key_pair" "test" {
+ identities = [{
+ name = "Test User"
+ email = "test_user@localhost.localdomain"
+ }]
+ passphrase = var.gpg_key_password
+}
+
+# Forgejo GPG key
+resource "forgejo_gpg_key" "this" {
+ # There is no user for this resource, GPG keys can only be managed for the authenticated user.
+ armored_public_key = gpg_key_pair.test.public_key
+}
diff --git a/go.mod b/go.mod
index 921acb4..f824384 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,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
diff --git a/go.sum b/go.sum
index b94d839..157d3bf 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ 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/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=
diff --git a/internal/provider/gpg_key_data_source.go b/internal/provider/gpg_key_data_source.go
new file mode 100644
index 0000000..0731ad1
--- /dev/null
+++ b/internal/provider/gpg_key_data_source.go
@@ -0,0 +1,242 @@
+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 {
+ User types.String `tfsdk:"user"`
+ KeyID types.String `tfsdk:"key_id"`
+ ID types.Int64 `tfsdk:"id"`
+ PrimaryKeyID types.String `tfsdk:"primary_key_id"`
+ PublicKey types.String `tfsdk:"public_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"`
+ Emails types.List `tfsdk:"emails"`
+ Subkeys types.List `tfsdk:"subkeys"`
+}
+
+// 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{
+ "key_id": schema.StringAttribute{
+ Description: "ID of the GPG key.",
+ Required: true,
+ },
+ "user": schema.StringAttribute{
+ Description: "Name of the user.",
+ Optional: true,
+ },
+ "id": schema.Int64Attribute{
+ Description: "Numeric identifier of the GPG key.",
+ Computed: true,
+ },
+ "primary_key_id": schema.StringAttribute{
+ Description: "Primary ID of the GPG key.",
+ Computed: true,
+ },
+ "public_key": schema.StringAttribute{
+ Description: "The public 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,
+ },
+ "emails": schema.ListAttribute{
+ Description: "Emails associated with the GPG key.",
+ Computed: true,
+ ElementType: gpgKeyEmailType,
+ },
+ "subkeys": schema.ListAttribute{
+ Description: "Subkeys of the GPG key.",
+ Computed: true,
+ ElementType: gpgKeySubkeyType,
+ },
+ },
+ }
+}
+
+// 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
+ }
+
+ user := data.User.ValueString()
+
+ tflog.Info(ctx, "List GPG keys", map[string]any{
+ "user": user,
+ })
+
+ // Use Forgejo client to list GPG keys
+ var keys []*forgejo.GPGKey
+ var res *forgejo.Response
+ var err error
+ if user != "" {
+ keys, res, err = d.client.ListGPGKeys(
+ user,
+ forgejo.ListGPGKeysOptions{},
+ )
+ } else {
+ 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:
+ // 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`,
+ user,
+ 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 {
+ var msg string
+ if user != "" {
+ msg = fmt.Sprintf(
+ `GPG key with user "%s" and key_id %s not found.`,
+ user,
+ data.KeyID.String(),
+ )
+ } else {
+ msg = fmt.Sprintf(
+ "GPG key with key_id %s not found.",
+ data.KeyID.String(),
+ )
+ }
+ resp.Diagnostics.AddError("Unable to get GPG key by key_id", msg)
+
+ 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.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.String())
+ data.Expires = types.StringValue(keys[idx].Expires.String())
+
+ data.Emails, diags = getEmails(keys[idx])
+ resp.Diagnostics.Append(diags...)
+
+ data.Subkeys, diags = getSubkeys(keys[idx])
+ resp.Diagnostics.Append(diags...)
+
+ // 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{}
+}
diff --git a/internal/provider/gpg_key_data_source_test.go b/internal/provider/gpg_key_data_source_test.go
new file mode 100644
index 0000000..5dbfd13
--- /dev/null
+++ b/internal/provider/gpg_key_data_source_test.go
@@ -0,0 +1,127 @@
+package provider_test
+
+import (
+ "fmt"
+ "regexp"
+ "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) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "gpg": {
+ Source: "terraform-provider-gpg/gpg",
+ },
+ },
+ 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 (valid user, non-existent resource)
+ {
+ Config: providerConfig + `
+data "forgejo_gpg_key" "test" {
+ user = "tfadmin"
+ key_id = "non_existent"
+}`,
+ ExpectError: regexp.MustCompile(`GPG key with user "tfadmin" and key_id "non_existent" not found`),
+ },
+ // Read testing (non-existent user)
+ {
+ Config: providerConfig + `
+data "forgejo_gpg_key" "test" {
+ user = "invalid"
+ key_id = "non_existent"
+}`,
+ ExpectError: regexp.MustCompile(`GPG keys for user "invalid" not found`),
+ },
+ // Read testing (current user)
+ {
+ Config: providerConfig + fmt.Sprintf(`
+resource "gpg_key_pair" "test" {
+ identities = [{
+ name = "TF Admin"
+ email = "%s"
+ }]
+ passphrase = "supersecret"
+}
+resource "forgejo_gpg_key" "test" {
+ armored_public_key = gpg_key_pair.test.public_key
+}
+data "forgejo_gpg_key" "test" {
+ key_id = gpg_key_pair.test.id
+
+ depends_on = [forgejo_gpg_key.test]
+}`, forgejoEmail),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("primary_key_id"), knownvalue.StringExact("")),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("public_key"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("can_sign"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("can_encrypt_comms"), knownvalue.Bool(false)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("can_encrypt_storage"), knownvalue.Bool(false)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("can_certify"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("created_at"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("expires_at"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("emails").AtSliceIndex(0).AtMapKey("email"), knownvalue.StringExact(forgejoEmail)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("emails").AtSliceIndex(0).AtMapKey("verified"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("primary_key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("public_key"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_sign"), knownvalue.Bool(false)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_encrypt_comms"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_encrypt_storage"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_certify"), knownvalue.Bool(false)),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("created_at"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("data.forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("expires_at"), knownvalue.NotNull()),
+ },
+ },
+ // Read testing (explicit user)
+ {
+ Config: providerConfig + fmt.Sprintf(`
+resource "gpg_key_pair" "test" {
+ identities = [{
+ name = "TF Admin"
+ email = "%s"
+ }]
+ passphrase = "supersecret"
+}
+resource "forgejo_gpg_key" "test" {
+ armored_public_key = gpg_key_pair.test.public_key
+}
+data "forgejo_gpg_key" "test" {
+ user = "tfadmin"
+ key_id = gpg_key_pair.test.id
+
+ depends_on = [forgejo_gpg_key.test]
+}`, forgejoEmail),
+ 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.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("primary_key_id"), knownvalue.StringExact("")),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("public_key"), 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()),
+ },
+ },
+ },
+ })
+}
diff --git a/internal/provider/gpg_key_resource.go b/internal/provider/gpg_key_resource.go
new file mode 100644
index 0000000..19eee52
--- /dev/null
+++ b/internal/provider/gpg_key_resource.go
@@ -0,0 +1,478 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "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 (
+ _ resource.Resource = &gpgKeyResource{}
+ _ resource.ResourceWithConfigure = &gpgKeyResource{}
+)
+
+// Nested resource types.
+var (
+ gpgKeyEmailAttrTypes = map[string]attr.Type{
+ "email": types.StringType,
+ "verified": types.BoolType,
+ }
+ gpgKeyEmailType = types.ObjectType{
+ AttrTypes: gpgKeyEmailAttrTypes,
+ }
+
+ gpgKeySubkeyAttrTypes = map[string]attr.Type{
+ "id": types.Int64Type,
+ "primary_key_id": types.StringType,
+ "key_id": types.StringType,
+ "public_key": types.StringType,
+ "can_sign": types.BoolType,
+ "can_encrypt_comms": types.BoolType,
+ "can_encrypt_storage": types.BoolType,
+ "can_certify": types.BoolType,
+ "created_at": types.StringType,
+ "expires_at": types.StringType,
+ }
+ gpgKeySubkeyType = types.ObjectType{
+ AttrTypes: gpgKeySubkeyAttrTypes,
+ }
+)
+
+// gpgKeyResource is the resource implementation.
+type gpgKeyResource struct {
+ client *forgejo.Client
+}
+
+// gpgKeyResourceModel maps the resource schema data.
+// https://pkg.go.dev/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2#GPGKey
+type gpgKeyResourceModel struct {
+ ArmoredPublicKey types.String `tfsdk:"armored_public_key"`
+ ID types.Int64 `tfsdk:"id"`
+ KeyID types.String `tfsdk:"key_id"`
+ PrimaryKeyID types.String `tfsdk:"primary_key_id"`
+ PublicKey types.String `tfsdk:"public_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"`
+ Emails types.List `tfsdk:"emails"`
+ SubKeys types.List `tfsdk:"subkeys"`
+}
+
+// from is a helper function to load an API struct into Terraform data model.
+func (m *gpgKeyResourceModel) from(k *forgejo.GPGKey) diag.Diagnostics {
+ m.ID = types.Int64Value(k.ID)
+ m.KeyID = types.StringValue(k.KeyID)
+ m.PrimaryKeyID = types.StringValue(k.PrimaryKeyID)
+ m.PublicKey = types.StringValue(k.PublicKey)
+ m.CanSign = types.BoolValue(k.CanSign)
+ m.CanEncryptComms = types.BoolValue(k.CanEncryptComms)
+ m.CanEncryptStorage = types.BoolValue(k.CanEncryptStorage)
+ m.CanCertify = types.BoolValue(k.CanCertify)
+ m.Created = types.StringValue(k.Created.Format(time.RFC3339))
+ m.Expires = types.StringValue(k.Expires.Format(time.RFC3339))
+
+ emails, diags := getEmails(k)
+ if diags.HasError() {
+ return diags
+ }
+ m.Emails = emails
+
+ subkeys, diags := getSubkeys(k)
+ if diags.HasError() {
+ return diags
+ }
+ m.SubKeys = subkeys
+
+ return nil
+}
+
+// to is a helper function to save Terraform data model into an API struct.
+func (m *gpgKeyResourceModel) to(o *forgejo.CreateGPGKeyOption) {
+ if o == nil {
+ o = new(forgejo.CreateGPGKeyOption)
+ }
+
+ o.ArmoredKey = m.ArmoredPublicKey.ValueString()
+}
+
+func getEmails(k *forgejo.GPGKey) (types.List, diag.Diagnostics) {
+ emailElements := make([]attr.Value, 0, len(k.Emails))
+
+ for _, e := range k.Emails {
+ values := map[string]attr.Value{
+ "email": types.StringValue(e.Email),
+ "verified": types.BoolValue(e.Verified),
+ }
+ elem, diags := types.ObjectValue(gpgKeyEmailAttrTypes, values)
+
+ if diags.HasError() {
+ return types.List{}, diags
+ }
+
+ emailElements = append(emailElements, elem)
+ }
+
+ emails, diags := types.ListValue(gpgKeyEmailType, emailElements)
+ if diags.HasError() {
+ return types.List{}, diags
+ }
+
+ return emails, nil
+}
+
+func getSubkeys(k *forgejo.GPGKey) (types.List, diag.Diagnostics) {
+ subkeyElements := make([]attr.Value, 0, len(k.SubsKey))
+
+ for _, e := range k.SubsKey {
+ values := map[string]attr.Value{
+ "id": types.Int64Value(e.ID),
+ "primary_key_id": types.StringValue(e.PrimaryKeyID),
+ "key_id": types.StringValue(e.KeyID),
+ "public_key": types.StringValue(e.PublicKey),
+ "can_sign": types.BoolValue(e.CanSign),
+ "can_encrypt_comms": types.BoolValue(e.CanEncryptComms),
+ "can_encrypt_storage": types.BoolValue(e.CanEncryptStorage),
+ "can_certify": types.BoolValue(e.CanCertify),
+ "created_at": types.StringValue(e.Created.Format(time.RFC3339)),
+ "expires_at": types.StringValue(e.Expires.Format(time.RFC3339)),
+ }
+ elem, diags := types.ObjectValue(gpgKeySubkeyAttrTypes, values)
+
+ if diags.HasError() {
+ return types.List{}, diags
+ }
+
+ subkeyElements = append(subkeyElements, elem)
+ }
+
+ subkeys, diags := types.ListValue(gpgKeySubkeyType, subkeyElements)
+ if diags.HasError() {
+ return types.List{}, diags
+ }
+
+ return subkeys, nil
+}
+
+// Metadata returns the resource type name.
+func (r *gpgKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_gpg_key"
+}
+
+// Schema defines the schema for the resource.
+func (r *gpgKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "Forgejo user GPG key resource.",
+
+ Attributes: map[string]schema.Attribute{
+ "armored_public_key": schema.StringAttribute{
+ Description: "Armored GPG Public key.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "id": schema.Int64Attribute{
+ Description: "Numeric identifier of the GPG key.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "key_id": schema.StringAttribute{
+ Description: "ID of the GPG key.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "primary_key_id": schema.StringAttribute{
+ Description: "Primary ID of the GPG key.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "public_key": schema.StringAttribute{
+ Description: "The public key.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "can_sign": schema.BoolAttribute{
+ Description: "Can this key sign.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "can_encrypt_comms": schema.BoolAttribute{
+ Description: "Can this key encrypt communications.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "can_encrypt_storage": schema.BoolAttribute{
+ Description: "Can this key encrypt storage.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "can_certify": schema.BoolAttribute{
+ Description: "Can this key certify.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "created_at": schema.StringAttribute{
+ Description: "Time at which the GPG key was created.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "expires_at": schema.StringAttribute{
+ Description: "Time at which the GPG key expires.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "emails": schema.ListAttribute{
+ Description: "Emails associated with the GPG key.",
+ Computed: true,
+ ElementType: gpgKeyEmailType,
+ PlanModifiers: []planmodifier.List{
+ listplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "subkeys": schema.ListAttribute{
+ Description: "Subkeys of the GPG key.",
+ Computed: true,
+ ElementType: gpgKeySubkeyType,
+ PlanModifiers: []planmodifier.List{
+ listplanmodifier.UseStateForUnknown(),
+ },
+ },
+ },
+ }
+}
+
+// Configure adds the provider configured client to the resource.
+func (r *gpgKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.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 Resource Configure Type",
+ fmt.Sprintf(
+ "Expected *forgejo.Client, got: %T. Please report this issue to the provider developers.",
+ req.ProviderData,
+ ),
+ )
+
+ return
+ }
+
+ r.client = client
+}
+
+// Create creates the resource and sets the initial Terraform state.
+func (r *gpgKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ defer un(trace(ctx, "Create GPG key resource"))
+
+ var data gpgKeyResourceModel
+
+ // Read Terraform plan data into model
+ diags := req.Plan.Get(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Info(ctx, "Create GPG key", map[string]any{
+ "armored_public_key": data.ArmoredPublicKey.ValueString(),
+ })
+
+ // Generate API request body from plan
+ opts := forgejo.CreateGPGKeyOption{}
+ data.to(&opts)
+
+ // Use Forgejo client to create new GPG key
+ key, res, err := r.client.CreateGPGKey(opts)
+ if err != nil {
+ tflog.Error(ctx, "Error", map[string]any{
+ "status": res.Status,
+ })
+
+ var msg string
+ switch res.StatusCode {
+ case 403:
+ msg = fmt.Sprintf(
+ "GPG key creation forbidden: %s",
+ err,
+ )
+ case 404:
+ msg = fmt.Sprintf(
+ "GPG key creation not found: %s",
+ err,
+ )
+ case 422:
+ msg = fmt.Sprintf("Input validation error: %s", err)
+ default:
+ msg = fmt.Sprintf("Unknown error: %s", err)
+ }
+ resp.Diagnostics.AddError("Unable to create GPG key", msg)
+
+ return
+ }
+
+ // Map response body to model
+ diags = data.from(key)
+ resp.Diagnostics.Append(diags...)
+
+ // Save data into Terraform state
+ diags = resp.State.Set(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (r *gpgKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ defer un(trace(ctx, "Read GPG key resource"))
+
+ var data gpgKeyResourceModel
+
+ // Read Terraform prior state data into the model
+ diags := req.State.Get(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Info(ctx, "Get GPG key by id", map[string]any{
+ "key_id": data.KeyID.ValueString(),
+ })
+
+ // Use Forgejo client to get GPG key
+ key, res, err := r.client.GetGPGKey(data.ID.ValueInt64())
+ if err != nil {
+ tflog.Error(ctx, "Error", map[string]any{
+ "status": res.Status,
+ })
+
+ var msg string
+ switch res.StatusCode {
+ case 403:
+ msg = fmt.Sprintf(
+ "GPG key with id %d forbidden: %s",
+ data.ID.ValueInt64(),
+ err,
+ )
+ case 404:
+ msg = fmt.Sprintf(
+ "GPG key with id %d not found: %s",
+ data.ID.ValueInt64(),
+ err,
+ )
+ default:
+ msg = fmt.Sprintf("Unknown error: %s", err)
+ }
+ resp.Diagnostics.AddError("Unable to get GPG key by id", msg)
+
+ return
+ }
+
+ // Map response body to model
+ data.from(key)
+
+ // Save data into Terraform state
+ diags = resp.State.Set(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *gpgKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ defer un(trace(ctx, "Update GPG key resource"))
+
+ /*
+ * GPG keys can not be updated in-place. All writable attributes have
+ * 'RequiresReplace' plan modifier set.
+ */
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *gpgKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ defer un(trace(ctx, "Delete GPG key resource"))
+
+ var data gpgKeyResourceModel
+
+ // Read Terraform prior state data into the model
+ diags := req.State.Get(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Info(ctx, "Delete GPG key", map[string]any{
+ "key_id": data.KeyID.ValueString(),
+ })
+
+ // Use Forgejo client to delete existing GPG key
+ res, err := r.client.DeleteGPGKey(data.ID.ValueInt64())
+ if err != nil {
+ tflog.Error(ctx, "Error", map[string]any{
+ "status": res.Status,
+ })
+
+ var msg string
+ switch res.StatusCode {
+ case 403:
+ msg = fmt.Sprintf(
+ "GPG key with id %d forbidden: %s",
+ data.ID.ValueInt64(),
+ err,
+ )
+ case 404:
+ msg = fmt.Sprintf(
+ "GPG key with id %d not found: %s",
+ data.ID.ValueInt64(),
+ err,
+ )
+ default:
+ msg = fmt.Sprintf("Unknown error: %s", err)
+ }
+ resp.Diagnostics.AddError("Unable to delete GPG key", msg)
+
+ return
+ }
+}
+
+// NewGPGKeyResource is a helper function to simplify the provider implementation.
+func NewGPGKeyResource() resource.Resource {
+ return &gpgKeyResource{}
+}
diff --git a/internal/provider/gpg_key_resource_test.go b/internal/provider/gpg_key_resource_test.go
new file mode 100644
index 0000000..80aadd6
--- /dev/null
+++ b/internal/provider/gpg_key_resource_test.go
@@ -0,0 +1,106 @@
+package provider_test
+
+import (
+ "fmt"
+ "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"
+)
+
+const forgejoEmail = "tfadmin@localhost"
+
+func TestAccGPGKeyResource(t *testing.T) {
+ validateIsArmoredGPGKey := func(value string) error {
+ lines := strings.Split(value, "\n")
+ if lines[0] != "-----BEGIN PGP PUBLIC KEY BLOCK-----" {
+ return fmt.Errorf(`expected "%s" to start with "-----BEGIN PGP PUBLIC KEY BLOCK-----"`, value)
+ }
+ if lines[len(lines)-1] != "-----END PGP PUBLIC KEY BLOCK-----" {
+ return fmt.Errorf(`expected "%s" to end with "-----END PGP PUBLIC KEY BLOCK-----"`, value)
+ }
+ return nil
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "gpg": {
+ Source: "terraform-provider-gpg/gpg",
+ },
+ },
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: providerConfig + fmt.Sprintf(`
+resource "gpg_key_pair" "test" {
+ identities = [{
+ name = "TF Admin"
+ email = "%s"
+ }]
+ passphrase = "supersecret"
+}
+resource "forgejo_gpg_key" "test" {
+ armored_public_key = gpg_key_pair.test.public_key
+}`, forgejoEmail),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("armored_public_key"), knownvalue.StringFunc(validateIsArmoredGPGKey)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("primary_key_id"), knownvalue.StringExact("")),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("public_key"), 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()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("emails").AtSliceIndex(0).AtMapKey("email"), knownvalue.StringExact(forgejoEmail)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("emails").AtSliceIndex(0).AtMapKey("verified"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("primary_key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("public_key"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_sign"), knownvalue.Bool(false)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_encrypt_comms"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_encrypt_storage"), knownvalue.Bool(true)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("can_certify"), knownvalue.Bool(false)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("created_at"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("subkeys").AtSliceIndex(0).AtMapKey("expires_at"), knownvalue.NotNull()),
+ },
+ },
+ // Recreate and Read testing
+ {
+ Config: providerConfig + fmt.Sprintf(`
+resource "gpg_key_pair" "test" {
+ identities = [{
+ name = "TF Admin"
+ email = "%s"
+ }]
+ passphrase = "supersecret"
+}
+resource "forgejo_gpg_key" "test" {
+ armored_public_key = gpg_key_pair.test.public_key
+}`, forgejoEmail),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("armored_public_key"), knownvalue.StringFunc(validateIsArmoredGPGKey)),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("key_id"), knownvalue.NotNull()),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("primary_key_id"), knownvalue.StringExact("")),
+ statecheck.ExpectKnownValue("forgejo_gpg_key.test", tfjsonpath.New("public_key"), 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()),
+ },
+ },
+ // Delete testing automatically occurs in TestCase
+ },
+ })
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 14dc8d3..89fb654 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -241,6 +241,7 @@ func (p *forgejoProvider) DataSources(_ context.Context) []func() datasource.Dat
return []func() datasource.DataSource{
NewCollaboratorDataSource,
NewDeployKeyDataSource,
+ NewGPGKeyDataSource,
NewOrganizationDataSource,
NewRepositoryDataSource,
NewSSHKeyDataSource,
@@ -253,6 +254,7 @@ func (p *forgejoProvider) Resources(_ context.Context) []func() resource.Resourc
return []func() resource.Resource{
NewCollaboratorResource,
NewDeployKeyResource,
+ NewGPGKeyResource,
NewOrganizationActionSecretResource,
NewOrganizationResource,
NewRepositoryActionSecretResource,