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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions github/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
145 changes: 93 additions & 52 deletions github/resource_github_user_ssh_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,27 @@ package github
import (
"context"
"errors"
"log"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/google/go-github/v84/github"
"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,
},

Description: "Manages a SSH key for the authenticated user.",

SchemaVersion: 1,
Schema: map[string]*schema.Schema{
"title": {
Type: schema.TypeString,
Expand All @@ -33,98 +36,136 @@ 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,
Computed: true,
Description: "The URL of the SSH key.",
},
"key_id": {
Type: schema.TypeInt,
Computed: true,
Description: "The unique identifier of the SSH key.",
},
"etag": {
Type: schema.TypeString,
Computed: true,
},
},

StateUpgraders: []schema.StateUpgrader{
{
Version: 0,
Type: resourceGithubUserSshKeyV0().CoreConfigSchema().ImpliedType(),
Upgrade: resourceGithubUserSshKeyStateUpgradeV0,
},
},
}
}

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{
userKey, resp, err := client.Users.CreateKey(ctx, &github.Key{
Title: new(title),
Key: new(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("url", userKey.GetURL()); err != nil {
return diag.FromErr(err)
}
if err = d.Set("title", userKey.GetTitle()); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubUserSshKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v3client

keyID := int64(d.Get("key_id").(int))
userKey, resp, err := client.Users.GetKey(ctx, keyID)
if err != nil {
return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "user SSH key (%d)", keyID))
}

// set computed fields
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 resourceGithubUserSshKeyRead(d, meta)
return nil
}

func resourceGithubUserSshKeyRead(d *schema.ResourceData, meta any) error {
func resourceGithubUserSshKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v3client

id, err := strconv.ParseInt(d.Id(), 10, 64)
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 err != nil {
return unconvertibleIdErr(d.Id(), err)
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil
}
return diag.FromErr(err)
}
ctx := context.WithValue(context.Background(), ctxId, d.Id())
if !d.IsNewResource() {
ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string))
return nil
}

func resourceGithubUserSshKeyImport(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 key ID format: %w", err)
}

key, resp, err := client.Users.GetKey(ctx, id)
key, _, 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 {
log.Printf("[INFO] Removing user SSH key %s from state because it no longer exists in GitHub",
d.Id())
d.SetId("")
return nil
return nil, fmt.Errorf("SSH key with ID %d not found", keyID)
}
}
return err
return nil, err
}

if err = d.Set("etag", resp.Header.Get("ETag")); err != nil {
return err
if err = d.Set("key_id", key.GetID()); err != nil {
return nil, err
}
if err = d.Set("title", key.GetTitle()); err != nil {
return err
return nil, err
}
if err = d.Set("key", key.GetKey()); err != nil {
return err
}
if err = d.Set("url", key.GetURL()); err != nil {
return err
}

return nil
}

func resourceGithubUserSshKeyDelete(d *schema.ResourceData, meta any) error {
client := meta.(*Owner).v3client

id, err := strconv.ParseInt(d.Id(), 10, 64)
if err != nil {
return unconvertibleIdErr(d.Id(), err)
return nil, err
}
ctx := context.WithValue(context.Background(), ctxId, d.Id())

_, err = client.Users.DeleteKey(ctx, id)
return err
return []*schema.ResourceData{d}, nil
}
60 changes: 60 additions & 0 deletions github/resource_github_user_ssh_key_migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package github

import (
"context"
"fmt"
"strconv"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceGithubUserSshKeyV0() *schema.Resource {
return &schema.Resource{
SchemaVersion: 0,
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
}

return rawState, nil
}
Loading