From e2673ee6b9d3aac8cf3e0a377e8026c2ccd0edcd Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Mon, 26 Aug 2024 03:20:45 +0200 Subject: [PATCH 1/8] feat: add github_user_ssh_signing_key feat: add docs for github_user_ssh_signing_key fix: add github_user_ssh_signing_key to provider.go fix: add github_user_ssh_signing_key to github.erb fix: use tflog, context, test-structure fix: use resource-prefix, sweeper, 404-handling, direct read fix: key_id feat: import feat: adjust user_ssh_key to new structure fix: typo fix: dont overwrite key on create fix: add new fields to website --- github/acc_test.go | 70 +++++++++ github/provider.go | 1 + github/resource_github_user_ssh_key.go | 130 +++++++++------- github/resource_github_user_ssh_key_test.go | 33 ++-- .../resource_github_user_ssh_signing_key.go | 146 ++++++++++++++++++ ...source_github_user_ssh_signing_key_test.go | 85 ++++++++++ website/docs/r/user_ssh_key.html.markdown | 6 +- .../docs/r/user_ssh_signing_key.html.markdown | 43 ++++++ website/github.erb | 3 + 9 files changed, 437 insertions(+), 80 deletions(-) create mode 100644 github/resource_github_user_ssh_signing_key.go create mode 100644 github/resource_github_user_ssh_signing_key_test.go create mode 100644 website/docs/r/user_ssh_signing_key.html.markdown diff --git a/github/acc_test.go b/github/acc_test.go index eb7b5644ef..c12698c833 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -214,6 +214,16 @@ func configureSweepers() { Name: "teams", F: sweepTeams, }) + + resource.AddTestSweepers("user_ssh_keys", &resource.Sweeper{ + Name: "user_ssh_keys", + F: sweepUserSSHKeys, + }) + + resource.AddTestSweepers("user_ssh_signing_keys", &resource.Sweeper{ + Name: "user_ssh_signing_keys", + F: sweepUserSSHSigningKeys, + }) } func sweepTeams(_ string) error { @@ -286,6 +296,66 @@ func sweepRepositories(_ string) error { return nil } +func sweepUserSSHKeys(_ string) error { + fmt.Println("sweeping user SSH keys") + + meta, err := getTestMeta() + if err != nil { + return fmt.Errorf("could not get test meta for sweeper: %w", err) + } + + client := meta.v3client + owner := meta.name + ctx := context.Background() + + keys, _, err := client.Users.ListKeys(ctx, owner, nil) + if err != nil { + return err + } + + for _, k := range keys { + if title := k.GetTitle(); strings.HasPrefix(title, testResourcePrefix) { + fmt.Printf("destroying user SSH key %s\n", title) + + if _, err := client.Users.DeleteKey(ctx, k.GetID()); err != nil { + return err + } + } + } + + return nil +} + +func sweepUserSSHSigningKeys(_ string) error { + fmt.Println("sweeping user SSH signing keys") + + meta, err := getTestMeta() + if err != nil { + return fmt.Errorf("could not get test meta for sweeper: %w", err) + } + + client := meta.v3client + owner := meta.name + ctx := context.Background() + + keys, _, err := client.Users.ListSSHSigningKeys(ctx, owner, nil) + if err != nil { + return err + } + + for _, k := range keys { + if title := k.GetTitle(); strings.HasPrefix(title, testResourcePrefix) { + fmt.Printf("destroying user SSH signing key %s\n", title) + + if _, err := client.Users.DeleteSSHSigningKey(ctx, k.GetID()); err != nil { + return err + } + } + } + + return nil +} + func skipUnauthenticated(t *testing.T) { if testAccConf.authMode == anonymous { t.Skip("Skipping as test mode not authenticated") diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..c87888e9d4 100644 --- a/github/provider.go +++ b/github/provider.go @@ -212,6 +212,7 @@ func Provider() *schema.Provider { "github_user_gpg_key": resourceGithubUserGpgKey(), "github_user_invitation_accepter": resourceGithubUserInvitationAccepter(), "github_user_ssh_key": resourceGithubUserSshKey(), + "github_user_ssh_signing_key": resourceGithubUserSshSigningKey(), "github_enterprise_organization": resourceGithubEnterpriseOrganization(), "github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(), "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index 969938ceca..329eeadbb5 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -2,23 +2,23 @@ package github import ( "context" - "errors" - "log" + "fmt" "net/http" "strconv" - "strings" "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubUserSshKey() *schema.Resource { return &schema.Resource{ - Create: resourceGithubUserSshKeyCreate, - Read: resourceGithubUserSshKeyRead, - Delete: resourceGithubUserSshKeyDelete, + CreateContext: resourceGithubUserSshKeyCreate, + ReadContext: resourceGithubUserSshKeyRead, + DeleteContext: resourceGithubUserSshKeyDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubUserSshKeyImport, }, Schema: map[string]*schema.Schema{ @@ -33,15 +33,11 @@ func resourceGithubUserSshKey() *schema.Resource { Required: true, ForceNew: true, Description: "The public SSH key to add to your GitHub account.", - DiffSuppressFunc: func(k, oldV, newV string, d *schema.ResourceData) bool { - newTrimmed := strings.TrimSpace(newV) - return oldV == newTrimmed - }, }, - "url": { - Type: schema.TypeString, + "key_id": { + Type: schema.TypeInt, Computed: true, - Description: "The URL of the SSH key.", + Description: "The unique identifier of the SSH key.", }, "etag": { Type: schema.TypeString, @@ -51,80 +47,100 @@ func resourceGithubUserSshKey() *schema.Resource { } } -func resourceGithubUserSshKeyCreate(d *schema.ResourceData, meta any) error { +func resourceGithubUserSshKeyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client title := d.Get("title").(string) key := d.Get("key").(string) - ctx := context.Background() - userKey, _, err := client.Users.CreateKey(ctx, &github.Key{ - Title: new(title), - Key: new(key), + userKey, resp, err := client.Users.CreateKey(ctx, &github.Key{ + Title: github.Ptr(title), + Key: github.Ptr(key), }) if err != nil { - return err + return diag.FromErr(err) } - d.SetId(strconv.FormatInt(*userKey.ID, 10)) + d.SetId(strconv.FormatInt(userKey.GetID(), 10)) + + if err = d.Set("key_id", userKey.GetID()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + if err = d.Set("title", userKey.GetTitle()); err != nil { + return diag.FromErr(err) + } - return resourceGithubUserSshKeyRead(d, meta) + return nil } -func resourceGithubUserSshKeyRead(d *schema.ResourceData, meta any) error { +func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client - id, err := strconv.ParseInt(d.Id(), 10, 64) + keyID := d.Get("key_id").(int64) + _, _, err := client.Users.GetKey(ctx, keyID) if err != nil { - return unconvertibleIdErr(d.Id(), err) - } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - if !d.IsNewResource() { - ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) - } - - key, resp, err := client.Users.GetKey(ctx, id) - if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { + if ghErr, ok := err.(*github.ErrorResponse); ok { if ghErr.Response.StatusCode == http.StatusNotModified { return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing user SSH key %s from state because it no longer exists in GitHub", - d.Id()) + tflog.Info(ctx, fmt.Sprintf("Removing user SSH key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{ + "ssh_key_id": d.Id(), + }) d.SetId("") return nil } } - return err } + return nil +} - if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return err - } - if err = d.Set("title", key.GetTitle()); err != nil { - return err - } - if err = d.Set("key", key.GetKey()); err != nil { - return err - } - if err = d.Set("url", key.GetURL()); err != nil { - return err - } +func resourceGithubUserSshKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client - return nil + keyID := d.Get("key_id").(int64) + resp, err := client.Users.DeleteKey(ctx, keyID) + if resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) } -func resourceGithubUserSshKeyDelete(d *schema.ResourceData, meta any) error { +func resourceGithubUserSshKeyImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { client := meta.(*Owner).v3client - id, err := strconv.ParseInt(d.Id(), 10, 64) + keyID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid SSH key ID format: %v", err) + } + + key, resp, err := client.Users.GetKey(ctx, keyID) if err != nil { - return unconvertibleIdErr(d.Id(), err) + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("SSH key with ID %d not found", keyID) + } + } + return nil, err + } + + d.SetId(strconv.FormatInt(key.GetID(), 10)) + + if err = d.Set("key_id", key.GetID()); err != nil { + return nil, err + } + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return nil, err + } + if err = d.Set("title", key.GetTitle()); err != nil { + return nil, err + } + if err = d.Set("key", key.GetKey()); err != nil { + return nil, err } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - _, err = client.Users.DeleteKey(ctx, id) - return err + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_user_ssh_key_test.go b/github/resource_github_user_ssh_key_test.go index f21ac61239..090eda7619 100644 --- a/github/resource_github_user_ssh_key_test.go +++ b/github/resource_github_user_ssh_key_test.go @@ -16,27 +16,19 @@ import ( func TestAccGithubUserSshKey(t *testing.T) { t.Run("creates and destroys a user SSH key without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID) testKey := newTestKey() + config := fmt.Sprintf(` resource "github_user_ssh_key" "test" { - title = "tf-acc-test-%s" - key = "%s" + title = "%[1]s" + key = "%[2]s" } - `, randomID, testKey) + `, name, testKey) check := resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "github_user_ssh_key.test", "title", - regexp.MustCompile(randomID), - ), - resource.TestMatchResourceAttr( - "github_user_ssh_key.test", "key", - regexp.MustCompile("^ssh-rsa "), - ), - resource.TestMatchResourceAttr( - "github_user_ssh_key.test", "url", - regexp.MustCompile("^https://api.github.com/[a-z0-9]+/keys/"), - ), + resource.TestMatchResourceAttr("github_user_ssh_key.test", "title", regexp.MustCompile(randomID)), + resource.TestMatchResourceAttr("github_user_ssh_key.test", "key", regexp.MustCompile("^ssh-rsa ")), ) resource.Test(t, resource.TestCase{ @@ -53,13 +45,15 @@ func TestAccGithubUserSshKey(t *testing.T) { t.Run("imports an individual account SSH key without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID) testKey := newTestKey() + config := fmt.Sprintf(` resource "github_user_ssh_key" "test" { - title = "tf-acc-test-%s" - key = "%s" + title = "%[1]s" + key = "%[2]s" } - `, randomID, testKey) + `, name, testKey) check := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("github_user_ssh_key.test", "title"), @@ -87,6 +81,5 @@ func TestAccGithubUserSshKey(t *testing.T) { func newTestKey() string { privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) publicKey, _ := ssh.NewPublicKey(&privateKey.PublicKey) - testKey := strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n") - return testKey + return strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n") } diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go new file mode 100644 index 0000000000..76833ebf06 --- /dev/null +++ b/github/resource_github_user_ssh_signing_key.go @@ -0,0 +1,146 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubUserSshSigningKey() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceGithubUserSshSigningKeyCreate, + ReadContext: resourceGithubUserSshSigningKeyRead, + DeleteContext: resourceGithubUserSshSigningKeyDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubUserSshSigningKeyImport, + }, + + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "A descriptive name for the new key.", + }, + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The public SSH key to add to your GitHub account.", + }, + "key_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The unique identifier of the SSH signing key.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceGithubUserSshSigningKeyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + title := d.Get("title").(string) + key := d.Get("key").(string) + + userKey, resp, err := client.Users.CreateSSHSigningKey(ctx, &github.Key{ + Title: github.Ptr(title), + Key: github.Ptr(key), + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(userKey.GetID(), 10)) + + if err = d.Set("key_id", userKey.GetID()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + if err = d.Set("title", userKey.GetTitle()); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + keyID := d.Get("key_id").(int64) + _, _, err := client.Users.GetSSHSigningKey(ctx, keyID) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotModified { + return nil + } + if ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, fmt.Sprintf("Removing user SSH key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{ + "ssh_signing_key_id": d.Id(), + }) + d.SetId("") + return nil + } + } + } + return nil +} + +func resourceGithubUserSshSigningKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + keyID := d.Get("key_id").(int64) + resp, err := client.Users.DeleteSSHSigningKey(ctx, keyID) + if resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) +} + +func resourceGithubUserSshSigningKeyImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + client := meta.(*Owner).v3client + + keyID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid SSH signing key ID format: %v", err) + } + + key, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("SSH signing key with ID %d not found", keyID) + } + } + return nil, err + } + + d.SetId(strconv.FormatInt(key.GetID(), 10)) + + if err = d.Set("key_id", key.GetID()); err != nil { + return nil, err + } + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return nil, err + } + if err = d.Set("title", key.GetTitle()); err != nil { + return nil, err + } + if err = d.Set("key", key.GetKey()); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_user_ssh_signing_key_test.go b/github/resource_github_user_ssh_signing_key_test.go new file mode 100644 index 0000000000..08cb89642f --- /dev/null +++ b/github/resource_github_user_ssh_signing_key_test.go @@ -0,0 +1,85 @@ +package github + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "golang.org/x/crypto/ssh" +) + +func TestAccGithubUserSshSigningKey(t *testing.T) { + t.Run("creates and destroys a user SSH signing key without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID) + testKey := newTestSigningKey() + + config := fmt.Sprintf(` + resource "github_user_ssh_signing_key" "test" { + title = "%[1]s" + key = "%[2]s" + } + `, name, testKey) + + check := resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("github_user_ssh_signing_key.test", "title", regexp.MustCompile(randomID)), + resource.TestMatchResourceAttr("github_user_ssh_signing_key.test", "key", regexp.MustCompile("^ssh-rsa ")), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) + + t.Run("imports an individual account SSH signing key without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID) + testKey := newTestSigningKey() + + config := fmt.Sprintf(` + resource "github_user_ssh_signing_key" "test" { + title = "%[1]s" + key = "%[2]s" + } + `, name, testKey) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "title"), + resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "key"), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + ResourceName: "github_user_ssh_signing_key.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} + +func newTestSigningKey() string { + privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) + publicKey, _ := ssh.NewPublicKey(&privateKey.PublicKey) + return strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n") +} diff --git a/website/docs/r/user_ssh_key.html.markdown b/website/docs/r/user_ssh_key.html.markdown index 3827182bd1..23b2e11612 100644 --- a/website/docs/r/user_ssh_key.html.markdown +++ b/website/docs/r/user_ssh_key.html.markdown @@ -24,15 +24,15 @@ resource "github_user_ssh_key" "example" { The following arguments are supported: -* `title` - (Required) A descriptive name for the new key. e.g. `Personal MacBook Air` +* `title` - (Required) A descriptive name for the new key. * `key` - (Required) The public SSH key to add to your GitHub account. ## Attributes Reference The following attributes are exported: -* `id` - The ID of the SSH key -* `url` - The URL of the SSH key +* `key_id` - The unique identifier of the SSH signing key. +* `etag` ## Import diff --git a/website/docs/r/user_ssh_signing_key.html.markdown b/website/docs/r/user_ssh_signing_key.html.markdown new file mode 100644 index 0000000000..9075c3f327 --- /dev/null +++ b/website/docs/r/user_ssh_signing_key.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "github" +page_title: "GitHub: github_user_ssh_signing_key" +description: |- + Provides a GitHub user's SSH signing key resource. +--- + +# github_user_ssh_signing_key + +Provides a GitHub user's SSH signing key resource. + +This resource allows you to add/remove SSH signing keys from your user account. + +## Example Usage + +```hcl +resource "github_user_ssh_signing_key" "example" { + title = "example title" + key = file("~/.ssh/id_rsa.pub") +} +``` + +## Argument Reference + +The following arguments are supported: + +* `title` - (Required) A descriptive name for the new key. +* `key` - (Required) The public SSH signing key to add to your GitHub account. + +## Attributes Reference + +The following attributes are exported: + +* `key_id` - The unique identifier of the SSH signing key. +* `etag` + +## Import + +SSH signing keys can be imported using their ID e.g. + +``` +$ terraform import github_user_ssh_signing_key.example 1234567 +``` diff --git a/website/github.erb b/website/github.erb index 997536b42f..2285f531be 100644 --- a/website/github.erb +++ b/website/github.erb @@ -451,6 +451,9 @@
  • github_user_ssh_key
  • +
  • + github_user_ssh_signing_key +
  • From a70a32e2df71154860f974a9ba903a8c7e31f6b7 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Tue, 24 Feb 2026 15:43:39 +0100 Subject: [PATCH 2/8] fix: linting --- github/resource_github_user_ssh_key.go | 9 ++++++--- github/resource_github_user_ssh_signing_key.go | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index 329eeadbb5..a1e56e7f9b 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "fmt" "net/http" "strconv" @@ -82,7 +83,8 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m keyID := d.Get("key_id").(int64) _, _, err := client.Users.GetKey(ctx, keyID) if err != nil { - if ghErr, ok := err.(*github.ErrorResponse); ok { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotModified { return nil } @@ -114,12 +116,13 @@ func resourceGithubUserSshKeyImport(ctx context.Context, d *schema.ResourceData, keyID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return nil, fmt.Errorf("invalid SSH key ID format: %v", err) + return nil, fmt.Errorf("invalid SSH key ID format: %w", err) } key, resp, err := client.Users.GetKey(ctx, keyID) if err != nil { - if ghErr, ok := err.(*github.ErrorResponse); ok { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("SSH key with ID %d not found", keyID) } diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go index 76833ebf06..04b10dc2ba 100644 --- a/github/resource_github_user_ssh_signing_key.go +++ b/github/resource_github_user_ssh_signing_key.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "fmt" "net/http" "strconv" @@ -82,7 +83,8 @@ func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.Resource keyID := d.Get("key_id").(int64) _, _, err := client.Users.GetSSHSigningKey(ctx, keyID) if err != nil { - if ghErr, ok := err.(*github.ErrorResponse); ok { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotModified { return nil } @@ -114,12 +116,13 @@ func resourceGithubUserSshSigningKeyImport(ctx context.Context, d *schema.Resour keyID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return nil, fmt.Errorf("invalid SSH signing key ID format: %v", err) + return nil, fmt.Errorf("invalid SSH signing key ID format: %w", err) } key, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) if err != nil { - if ghErr, ok := err.(*github.ErrorResponse); ok { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("SSH signing key with ID %d not found", keyID) } From 596ca30cf474686ec8214e951ae4d1fbb6170bc3 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Tue, 24 Feb 2026 20:02:58 +0100 Subject: [PATCH 3/8] fix(tests): correct imports --- github/resource_github_user_ssh_signing_key_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/resource_github_user_ssh_signing_key_test.go b/github/resource_github_user_ssh_signing_key_test.go index 08cb89642f..b938c52550 100644 --- a/github/resource_github_user_ssh_signing_key_test.go +++ b/github/resource_github_user_ssh_signing_key_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" "golang.org/x/crypto/ssh" ) From 6315933e153af51f3a75ebbc71927ea6fccda348 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Sun, 15 Mar 2026 09:10:49 +0100 Subject: [PATCH 4/8] fix: int usage, keep url, state migration --- github/resource_github_user_ssh_key.go | 70 +++++++++++++++++-- .../resource_github_user_ssh_signing_key.go | 9 ++- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index a1e56e7f9b..c7339d3346 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/google/go-github/v84/github" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -22,6 +23,7 @@ func resourceGithubUserSshKey() *schema.Resource { StateContext: resourceGithubUserSshKeyImport, }, + SchemaVersion: 1, Schema: map[string]*schema.Schema{ "title": { Type: schema.TypeString, @@ -35,6 +37,12 @@ func resourceGithubUserSshKey() *schema.Resource { ForceNew: true, Description: "The public SSH key to add to your GitHub account.", }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The URL of the SSH key.", + Deprecated: "Use key_id instead.", + }, "key_id": { Type: schema.TypeInt, Computed: true, @@ -45,6 +53,37 @@ func resourceGithubUserSshKey() *schema.Resource { Computed: true, }, }, + + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: cty.Object(map[string]cty.Type{ + "id": cty.String, + "title": cty.String, + "key": cty.String, + "url": cty.String, + "etag": cty.String, + }), + Upgrade: func(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return nil, fmt.Errorf("resource state upgrade failed, state is nil") + } + + // copy d.Id() into key_id + if id, ok := rawState["id"].(string); ok { + keyID, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return nil, fmt.Errorf("resource state upgrade failed, invalid SSH key ID format: %w", err) + } + rawState["key_id"] = keyID + } else { + return nil, fmt.Errorf("resource state upgrade failed, missing or invalid 'id' field in state") + } + + return rawState, nil + }, + }, + }, } } @@ -80,7 +119,16 @@ func resourceGithubUserSshKeyCreate(ctx context.Context, d *schema.ResourceData, func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client - keyID := d.Get("key_id").(int64) + keyID := int64(d.Get("key_id").(int)) + // fallback to d.Id() for backward compatibility when key_id is not set + if keyID == 0 { + var err error + keyID, err = strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.FromErr(fmt.Errorf("invalid SSH key ID format: %w", err)) + } + } + _, _, err := client.Users.GetKey(ctx, keyID) if err != nil { var ghErr *github.ErrorResponse @@ -103,12 +151,24 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m func resourceGithubUserSshKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client - keyID := d.Get("key_id").(int64) + keyID := int64(d.Get("key_id").(int)) + // fallback to d.Id() for backward compatibility when key_id is not set + if keyID == 0 { + var err error + keyID, err = strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.FromErr(fmt.Errorf("invalid SSH key ID format: %w", err)) + } + } + resp, err := client.Users.DeleteKey(ctx, keyID) - if resp.StatusCode == http.StatusNotFound { - return nil + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) } - return diag.FromErr(err) + return nil } func resourceGithubUserSshKeyImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go index 04b10dc2ba..70a10dbf70 100644 --- a/github/resource_github_user_ssh_signing_key.go +++ b/github/resource_github_user_ssh_signing_key.go @@ -105,10 +105,13 @@ func resourceGithubUserSshSigningKeyDelete(ctx context.Context, d *schema.Resour keyID := d.Get("key_id").(int64) resp, err := client.Users.DeleteSSHSigningKey(ctx, keyID) - if resp.StatusCode == http.StatusNotFound { - return nil + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) } - return diag.FromErr(err) + return nil } func resourceGithubUserSshSigningKeyImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { From 814cff953d8ef901b0545a81038f98949092da4b Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Mon, 23 Mar 2026 21:07:52 +0100 Subject: [PATCH 5/8] fix: readd url --- github/resource_github_user_ssh_key.go | 67 ++++++------------- .../resource_github_user_ssh_key_migration.go | 62 +++++++++++++++++ ...urce_github_user_ssh_key_migration_test.go | 49 ++++++++++++++ .../resource_github_user_ssh_signing_key.go | 31 +++++---- 4 files changed, 149 insertions(+), 60 deletions(-) create mode 100644 github/resource_github_user_ssh_key_migration.go create mode 100644 github/resource_github_user_ssh_key_migration_test.go diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index c7339d3346..0b4c7cad26 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/google/go-github/v84/github" - "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -41,7 +40,6 @@ func resourceGithubUserSshKey() *schema.Resource { Type: schema.TypeString, Computed: true, Description: "The URL of the SSH key.", - Deprecated: "Use key_id instead.", }, "key_id": { Type: schema.TypeInt, @@ -57,31 +55,8 @@ func resourceGithubUserSshKey() *schema.Resource { StateUpgraders: []schema.StateUpgrader{ { Version: 0, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "title": cty.String, - "key": cty.String, - "url": cty.String, - "etag": cty.String, - }), - Upgrade: func(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { - if rawState == nil { - return nil, fmt.Errorf("resource state upgrade failed, state is nil") - } - - // copy d.Id() into key_id - if id, ok := rawState["id"].(string); ok { - keyID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - return nil, fmt.Errorf("resource state upgrade failed, invalid SSH key ID format: %w", err) - } - rawState["key_id"] = keyID - } else { - return nil, fmt.Errorf("resource state upgrade failed, missing or invalid 'id' field in state") - } - - return rawState, nil - }, + Type: resourceGithubUserSshKeyV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubUserSshKeyStateUpgradeV0, }, }, } @@ -94,8 +69,8 @@ func resourceGithubUserSshKeyCreate(ctx context.Context, d *schema.ResourceData, key := d.Get("key").(string) userKey, resp, err := client.Users.CreateKey(ctx, &github.Key{ - Title: github.Ptr(title), - Key: github.Ptr(key), + Title: new(title), + Key: new(key), }) if err != nil { return diag.FromErr(err) @@ -109,6 +84,9 @@ func resourceGithubUserSshKeyCreate(ctx context.Context, d *schema.ResourceData, if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { return diag.FromErr(err) } + if err = d.Set("url", userKey.GetURL()); err != nil { + return diag.FromErr(err) + } if err = d.Set("title", userKey.GetTitle()); err != nil { return diag.FromErr(err) } @@ -120,16 +98,7 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m client := meta.(*Owner).v3client keyID := int64(d.Get("key_id").(int)) - // fallback to d.Id() for backward compatibility when key_id is not set - if keyID == 0 { - var err error - keyID, err = strconv.ParseInt(d.Id(), 10, 64) - if err != nil { - return diag.FromErr(fmt.Errorf("invalid SSH key ID format: %w", err)) - } - } - - _, _, err := client.Users.GetKey(ctx, keyID) + userKey, resp, err := client.Users.GetKey(ctx, keyID) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { @@ -145,6 +114,18 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m } } } + + // set computed fields + if err = d.Set("key_id", userKey.GetID()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + if err = d.Set("url", userKey.GetURL()); err != nil { + return diag.FromErr(err) + } + return nil } @@ -179,7 +160,7 @@ func resourceGithubUserSshKeyImport(ctx context.Context, d *schema.ResourceData, return nil, fmt.Errorf("invalid SSH key ID format: %w", err) } - key, resp, err := client.Users.GetKey(ctx, keyID) + key, _, err := client.Users.GetKey(ctx, keyID) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { @@ -192,12 +173,6 @@ func resourceGithubUserSshKeyImport(ctx context.Context, d *schema.ResourceData, d.SetId(strconv.FormatInt(key.GetID(), 10)) - if err = d.Set("key_id", key.GetID()); err != nil { - return nil, err - } - if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return nil, err - } if err = d.Set("title", key.GetTitle()); err != nil { return nil, err } diff --git a/github/resource_github_user_ssh_key_migration.go b/github/resource_github_user_ssh_key_migration.go new file mode 100644 index 0000000000..54a0a8c718 --- /dev/null +++ b/github/resource_github_user_ssh_key_migration.go @@ -0,0 +1,62 @@ +package github + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubUserSshKeyV0() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "A descriptive name for the new key.", + }, + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The public SSH key to add to your GitHub account.", + DiffSuppressFunc: func(k, oldV, newV string, d *schema.ResourceData) bool { + newTrimmed := strings.TrimSpace(newV) + return oldV == newTrimmed + }, + }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The URL of the SSH key.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceGithubUserSshKeyStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + if rawState == nil { + return nil, fmt.Errorf("resource state upgrade failed, state is nil") + } + + // copy d.Id() into key_id + if id, ok := rawState["id"].(string); ok { + keyID, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return nil, fmt.Errorf("resource state upgrade failed, invalid SSH key ID format: %w", err) + } + rawState["key_id"] = keyID + } else { + return nil, fmt.Errorf("resource state upgrade failed, missing or invalid 'id' field in state") + } + + return rawState, nil +} diff --git a/github/resource_github_user_ssh_key_migration_test.go b/github/resource_github_user_ssh_key_migration_test.go new file mode 100644 index 0000000000..91417fb713 --- /dev/null +++ b/github/resource_github_user_ssh_key_migration_test.go @@ -0,0 +1,49 @@ +package github + +import ( + "context" + "reflect" + "testing" +) + +func Test_resourceGithubUserSshKeyStateUpgradeV0(t *testing.T) { + t.Parallel() + + for _, d := range []struct { + testName string + rawState map[string]any + want map[string]any + shouldError bool + }{ + { + testName: "migrates_v0_to_v1", + rawState: map[string]any{ + "id": "123", + "title": "test-key", + "key": "test-key-data", + "url": "test-url", + }, + want: map[string]any{ + "id": "123", + "key_id": int64(123), + "title": "test-key", + "key": "test-key-data", + "url": "test-url", + }, + shouldError: false, + }, + } { + t.Run(d.testName, func(t *testing.T) { + t.Parallel() + + got, err := resourceGithubUserSshKeyStateUpgradeV0(context.Background(), d.rawState, nil) + if (err != nil) != d.shouldError { + t.Fatalf("unexpected error state") + } + + if !d.shouldError && !reflect.DeepEqual(got, d.want) { + t.Fatalf("got %+v, want %+v", got, d.want) + } + }) + } +} diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go index 70a10dbf70..211a09f5b7 100644 --- a/github/resource_github_user_ssh_signing_key.go +++ b/github/resource_github_user_ssh_signing_key.go @@ -33,7 +33,7 @@ func resourceGithubUserSshSigningKey() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "The public SSH key to add to your GitHub account.", + Description: "The public SSH signing key to add to your GitHub account.", }, "key_id": { Type: schema.TypeInt, @@ -55,8 +55,8 @@ func resourceGithubUserSshSigningKeyCreate(ctx context.Context, d *schema.Resour key := d.Get("key").(string) userKey, resp, err := client.Users.CreateSSHSigningKey(ctx, &github.Key{ - Title: github.Ptr(title), - Key: github.Ptr(key), + Title: new(title), + Key: new(key), }) if err != nil { return diag.FromErr(err) @@ -80,8 +80,8 @@ func resourceGithubUserSshSigningKeyCreate(ctx context.Context, d *schema.Resour func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client - keyID := d.Get("key_id").(int64) - _, _, err := client.Users.GetSSHSigningKey(ctx, keyID) + keyID := int64(d.Get("key_id").(int)) + userKey, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { @@ -89,7 +89,7 @@ func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.Resource return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Removing user SSH key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{ + tflog.Info(ctx, fmt.Sprintf("Removing user SSH signing key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{ "ssh_signing_key_id": d.Id(), }) d.SetId("") @@ -97,13 +97,22 @@ func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.Resource } } } + + // set computed fields + if err = d.Set("key_id", userKey.GetID()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + return nil } func resourceGithubUserSshSigningKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client - keyID := d.Get("key_id").(int64) + keyID := int64(d.Get("key_id").(int)) resp, err := client.Users.DeleteSSHSigningKey(ctx, keyID) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { @@ -122,7 +131,7 @@ func resourceGithubUserSshSigningKeyImport(ctx context.Context, d *schema.Resour return nil, fmt.Errorf("invalid SSH signing key ID format: %w", err) } - key, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) + key, _, err := client.Users.GetSSHSigningKey(ctx, keyID) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { @@ -135,12 +144,6 @@ func resourceGithubUserSshSigningKeyImport(ctx context.Context, d *schema.Resour d.SetId(strconv.FormatInt(key.GetID(), 10)) - if err = d.Set("key_id", key.GetID()); err != nil { - return nil, err - } - if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return nil, err - } if err = d.Set("title", key.GetTitle()); err != nil { return nil, err } From e3d891ec684782594eb0ba00af26cd4d9487cf5f Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Wed, 25 Mar 2026 21:59:20 +0100 Subject: [PATCH 6/8] fix: docs, unneeded args, missing description --- github/resource_github_user_ssh_key.go | 4 +++- .../resource_github_user_ssh_key_migration.go | 2 -- github/resource_github_user_ssh_key_test.go | 24 +++++++++---------- .../resource_github_user_ssh_signing_key.go | 4 +++- ...source_github_user_ssh_signing_key_test.go | 24 +++++++++---------- website/docs/r/user_ssh_key.html.markdown | 2 ++ .../docs/r/user_ssh_signing_key.html.markdown | 1 + 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index 0b4c7cad26..5fb6e5e060 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -22,6 +22,8 @@ func resourceGithubUserSshKey() *schema.Resource { StateContext: resourceGithubUserSshKeyImport, }, + Description: "Manages a SSH key for the authenticated user.", + SchemaVersion: 1, Schema: map[string]*schema.Schema{ "title": { @@ -106,7 +108,7 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Removing user SSH key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{ + tflog.Info(ctx, "Removing user SSH key from state because it no longer exists in GitHub", map[string]any{ "ssh_key_id": d.Id(), }) d.SetId("") diff --git a/github/resource_github_user_ssh_key_migration.go b/github/resource_github_user_ssh_key_migration.go index 54a0a8c718..ed70c6290e 100644 --- a/github/resource_github_user_ssh_key_migration.go +++ b/github/resource_github_user_ssh_key_migration.go @@ -54,8 +54,6 @@ func resourceGithubUserSshKeyStateUpgradeV0(ctx context.Context, rawState map[st return nil, fmt.Errorf("resource state upgrade failed, invalid SSH key ID format: %w", err) } rawState["key_id"] = keyID - } else { - return nil, fmt.Errorf("resource state upgrade failed, missing or invalid 'id' field in state") } return rawState, nil diff --git a/github/resource_github_user_ssh_key_test.go b/github/resource_github_user_ssh_key_test.go index 090eda7619..ad0b6336f8 100644 --- a/github/resource_github_user_ssh_key_test.go +++ b/github/resource_github_user_ssh_key_test.go @@ -4,12 +4,14 @@ import ( "crypto/rand" "crypto/rsa" "fmt" - "regexp" "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "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" "golang.org/x/crypto/ssh" ) @@ -26,18 +28,16 @@ func TestAccGithubUserSshKey(t *testing.T) { } `, name, testKey) - check := resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr("github_user_ssh_key.test", "title", regexp.MustCompile(randomID)), - resource.TestMatchResourceAttr("github_user_ssh_key.test", "key", regexp.MustCompile("^ssh-rsa ")), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, }, }, }) @@ -55,18 +55,16 @@ func TestAccGithubUserSshKey(t *testing.T) { } `, name, testKey) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("github_user_ssh_key.test", "title"), - resource.TestCheckResourceAttrSet("github_user_ssh_key.test", "key"), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, }, { ResourceName: "github_user_ssh_key.test", diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go index 211a09f5b7..b9a6e1efde 100644 --- a/github/resource_github_user_ssh_signing_key.go +++ b/github/resource_github_user_ssh_signing_key.go @@ -22,6 +22,8 @@ func resourceGithubUserSshSigningKey() *schema.Resource { StateContext: resourceGithubUserSshSigningKeyImport, }, + Description: "Manages a SSH signing key for the authenticated user.", + Schema: map[string]*schema.Schema{ "title": { Type: schema.TypeString, @@ -89,7 +91,7 @@ func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.Resource return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, fmt.Sprintf("Removing user SSH signing key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{ + tflog.Info(ctx, "Removing user SSH signing key from state because it no longer exists in GitHub", map[string]any{ "ssh_signing_key_id": d.Id(), }) d.SetId("") diff --git a/github/resource_github_user_ssh_signing_key_test.go b/github/resource_github_user_ssh_signing_key_test.go index b938c52550..26ab4ab12f 100644 --- a/github/resource_github_user_ssh_signing_key_test.go +++ b/github/resource_github_user_ssh_signing_key_test.go @@ -4,12 +4,14 @@ import ( "crypto/rand" "crypto/rsa" "fmt" - "regexp" "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "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" "golang.org/x/crypto/ssh" ) @@ -26,18 +28,16 @@ func TestAccGithubUserSshSigningKey(t *testing.T) { } `, name, testKey) - check := resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr("github_user_ssh_signing_key.test", "title", regexp.MustCompile(randomID)), - resource.TestMatchResourceAttr("github_user_ssh_signing_key.test", "key", regexp.MustCompile("^ssh-rsa ")), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, }, }, }) @@ -55,18 +55,16 @@ func TestAccGithubUserSshSigningKey(t *testing.T) { } `, name, testKey) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "title"), - resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "key"), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, }, { ResourceName: "github_user_ssh_signing_key.test", diff --git a/website/docs/r/user_ssh_key.html.markdown b/website/docs/r/user_ssh_key.html.markdown index 23b2e11612..26359f115a 100644 --- a/website/docs/r/user_ssh_key.html.markdown +++ b/website/docs/r/user_ssh_key.html.markdown @@ -24,6 +24,8 @@ resource "github_user_ssh_key" "example" { The following arguments are supported: +* `id` - The ID of the SSH key +* `url` - The URL of the SSH key * `title` - (Required) A descriptive name for the new key. * `key` - (Required) The public SSH key to add to your GitHub account. diff --git a/website/docs/r/user_ssh_signing_key.html.markdown b/website/docs/r/user_ssh_signing_key.html.markdown index 9075c3f327..26ecfeafb7 100644 --- a/website/docs/r/user_ssh_signing_key.html.markdown +++ b/website/docs/r/user_ssh_signing_key.html.markdown @@ -24,6 +24,7 @@ resource "github_user_ssh_signing_key" "example" { The following arguments are supported: +* `id` - The ID of the SSH signing key * `title` - (Required) A descriptive name for the new key. * `key` - (Required) The public SSH signing key to add to your GitHub account. From 3fa41d6719f9b6b948345a940c8af1b03b6e31ad Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Wed, 25 Mar 2026 22:25:03 +0100 Subject: [PATCH 7/8] fix: use deleteResourceOn404AndSwallow304OtherwiseReturnError --- github/resource_github_user_ssh_key.go | 15 +-------------- github/resource_github_user_ssh_signing_key.go | 15 +-------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index 5fb6e5e060..5c9bb705dc 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/google/go-github/v84/github" - "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -102,19 +101,7 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m keyID := int64(d.Get("key_id").(int)) userKey, resp, err := client.Users.GetKey(ctx, keyID) if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { - if ghErr.Response.StatusCode == http.StatusNotModified { - return nil - } - if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, "Removing user SSH key from state because it no longer exists in GitHub", map[string]any{ - "ssh_key_id": d.Id(), - }) - d.SetId("") - return nil - } - } + return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "user SSH key (%d)", keyID)) } // set computed fields diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go index b9a6e1efde..d374b745fb 100644 --- a/github/resource_github_user_ssh_signing_key.go +++ b/github/resource_github_user_ssh_signing_key.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/google/go-github/v84/github" - "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -85,19 +84,7 @@ func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.Resource keyID := int64(d.Get("key_id").(int)) userKey, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { - if ghErr.Response.StatusCode == http.StatusNotModified { - return nil - } - if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, "Removing user SSH signing key from state because it no longer exists in GitHub", map[string]any{ - "ssh_signing_key_id": d.Id(), - }) - d.SetId("") - return nil - } - } + return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "user SSH signing key (%d)", keyID)) } // set computed fields From 8b9f2cad75d8c19382c44e4754b2517a8f552c6c Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Thu, 26 Mar 2026 07:24:03 +0100 Subject: [PATCH 8/8] fix: setting key_id, schemaVersion, website arguments --- github/resource_github_user_ssh_key.go | 8 +++----- github/resource_github_user_ssh_key_migration.go | 2 +- github/resource_github_user_ssh_signing_key.go | 10 ++++------ website/docs/r/user_ssh_key.html.markdown | 6 +++--- website/docs/r/user_ssh_signing_key.html.markdown | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index 5c9bb705dc..3ed423ff1d 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -105,9 +105,6 @@ func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, m } // set computed fields - if err = d.Set("key_id", userKey.GetID()); err != nil { - return diag.FromErr(err) - } if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { return diag.FromErr(err) } @@ -160,8 +157,9 @@ func resourceGithubUserSshKeyImport(ctx context.Context, d *schema.ResourceData, return nil, err } - d.SetId(strconv.FormatInt(key.GetID(), 10)) - + if err = d.Set("key_id", key.GetID()); err != nil { + return nil, err + } if err = d.Set("title", key.GetTitle()); err != nil { return nil, err } diff --git a/github/resource_github_user_ssh_key_migration.go b/github/resource_github_user_ssh_key_migration.go index ed70c6290e..4d85c0befb 100644 --- a/github/resource_github_user_ssh_key_migration.go +++ b/github/resource_github_user_ssh_key_migration.go @@ -11,7 +11,7 @@ import ( func resourceGithubUserSshKeyV0() *schema.Resource { return &schema.Resource{ - SchemaVersion: 1, + SchemaVersion: 0, Schema: map[string]*schema.Schema{ "title": { Type: schema.TypeString, diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go index d374b745fb..b953c21345 100644 --- a/github/resource_github_user_ssh_signing_key.go +++ b/github/resource_github_user_ssh_signing_key.go @@ -82,15 +82,12 @@ func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.Resource client := meta.(*Owner).v3client keyID := int64(d.Get("key_id").(int)) - userKey, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) + _, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) if err != nil { return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "user SSH signing key (%d)", keyID)) } // set computed fields - if err = d.Set("key_id", userKey.GetID()); err != nil { - return diag.FromErr(err) - } if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { return diag.FromErr(err) } @@ -131,8 +128,9 @@ func resourceGithubUserSshSigningKeyImport(ctx context.Context, d *schema.Resour return nil, err } - d.SetId(strconv.FormatInt(key.GetID(), 10)) - + if err = d.Set("key_id", key.GetID()); err != nil { + return nil, err + } if err = d.Set("title", key.GetTitle()); err != nil { return nil, err } diff --git a/website/docs/r/user_ssh_key.html.markdown b/website/docs/r/user_ssh_key.html.markdown index 26359f115a..500271a63b 100644 --- a/website/docs/r/user_ssh_key.html.markdown +++ b/website/docs/r/user_ssh_key.html.markdown @@ -24,8 +24,6 @@ resource "github_user_ssh_key" "example" { The following arguments are supported: -* `id` - The ID of the SSH key -* `url` - The URL of the SSH key * `title` - (Required) A descriptive name for the new key. * `key` - (Required) The public SSH key to add to your GitHub account. @@ -33,7 +31,9 @@ The following arguments are supported: The following attributes are exported: -* `key_id` - The unique identifier of the SSH signing key. +* `id` - The ID of the SSH key +* `url` - The URL of the SSH key +* `key_id` - The unique identifier of the SSH key. * `etag` ## Import diff --git a/website/docs/r/user_ssh_signing_key.html.markdown b/website/docs/r/user_ssh_signing_key.html.markdown index 26ecfeafb7..045a4d0d19 100644 --- a/website/docs/r/user_ssh_signing_key.html.markdown +++ b/website/docs/r/user_ssh_signing_key.html.markdown @@ -24,7 +24,6 @@ resource "github_user_ssh_signing_key" "example" { The following arguments are supported: -* `id` - The ID of the SSH signing key * `title` - (Required) A descriptive name for the new key. * `key` - (Required) The public SSH signing key to add to your GitHub account. @@ -32,6 +31,7 @@ The following arguments are supported: The following attributes are exported: +* `id` - The ID of the SSH signing key * `key_id` - The unique identifier of the SSH signing key. * `etag`