-
Notifications
You must be signed in to change notification settings - Fork 22
Add GPG Key resource and data source #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
f22c14d
3052b48
81de91d
b77cbce
6c35089
7a6b9cf
50da96b
7bb55e8
e453a03
c478182
6fad9dc
ed385ff
dd055ab
e212900
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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. |
| 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 { | ||
| ID types.Int64 `tfsdk:"id"` | ||
| KeyID types.String `tfsdk:"key_id"` | ||
| PrimaryKeyID types.String `tfsdk:"primary_key_id"` | ||
| Fingerprint types.String `tfsdk:"fingerprint"` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is called
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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{} | ||
| } | ||
| 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()), | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
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
EmailsandSubsKey... did you omit them deliberately?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.