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
123 changes: 71 additions & 52 deletions github/resource_github_user_ssh_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@ package github
import (
"context"
"errors"
"log"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/google/go-github/v83/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{
Expand All @@ -33,15 +34,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,
Expand All @@ -51,80 +48,102 @@ 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{
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)
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)
keyID := d.Get("key_id").(int64)
_, _, 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())
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: %w", err)
}

key, resp, err := client.Users.GetKey(ctx, keyID)
if err != nil {
return unconvertibleIdErr(d.Id(), err)
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)
}
}
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
}
33 changes: 13 additions & 20 deletions github/resource_github_user_ssh_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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"),
Expand Down Expand Up @@ -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")
}
Loading