Skip to content

Commit d271ca5

Browse files
feat: add github_user_ssh_signing_key
1 parent 0d53cb2 commit d271ca5

2 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"log"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/google/go-github/v63/github"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
)
13+
14+
func resourceGithubUserSshSigningKey() *schema.Resource {
15+
return &schema.Resource{
16+
Create: resourceGithubUserSshSigningKeyCreate,
17+
Read: resourceGithubUserSshSigningKeyRead,
18+
Delete: resourceGithubUserSshSigningKeyDelete,
19+
Importer: &schema.ResourceImporter{
20+
StateContext: schema.ImportStatePassthroughContext,
21+
},
22+
23+
Schema: map[string]*schema.Schema{
24+
"title": {
25+
Type: schema.TypeString,
26+
Required: true,
27+
ForceNew: true,
28+
Description: "A descriptive name for the new key.",
29+
},
30+
"key": {
31+
Type: schema.TypeString,
32+
Required: true,
33+
ForceNew: true,
34+
Description: "The public SSH key to add to your GitHub account.",
35+
DiffSuppressFunc: func(k, oldV, newV string, d *schema.ResourceData) bool {
36+
newTrimmed := strings.TrimSpace(newV)
37+
return oldV == newTrimmed
38+
},
39+
},
40+
"etag": {
41+
Type: schema.TypeString,
42+
Computed: true,
43+
},
44+
},
45+
}
46+
}
47+
48+
func resourceGithubUserSshSigningKeyCreate(d *schema.ResourceData, meta interface{}) error {
49+
client := meta.(*Owner).v3client
50+
51+
title := d.Get("title").(string)
52+
key := d.Get("key").(string)
53+
ctx := context.Background()
54+
55+
userKey, _, err := client.Users.CreateSSHSigningKey(ctx, &github.Key{
56+
Title: github.String(title),
57+
Key: github.String(key),
58+
})
59+
if err != nil {
60+
return err
61+
}
62+
63+
d.SetId(strconv.FormatInt(*userKey.ID, 10))
64+
65+
return resourceGithubUserSshSigningKeyRead(d, meta)
66+
}
67+
68+
func resourceGithubUserSshSigningKeyRead(d *schema.ResourceData, meta interface{}) error {
69+
client := meta.(*Owner).v3client
70+
71+
id, err := strconv.ParseInt(d.Id(), 10, 64)
72+
if err != nil {
73+
return unconvertibleIdErr(d.Id(), err)
74+
}
75+
ctx := context.WithValue(context.Background(), ctxId, d.Id())
76+
if !d.IsNewResource() {
77+
ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string))
78+
}
79+
80+
key, resp, err := client.Users.GetSSHSigningKey(ctx, id)
81+
if err != nil {
82+
if ghErr, ok := err.(*github.ErrorResponse); ok {
83+
if ghErr.Response.StatusCode == http.StatusNotModified {
84+
return nil
85+
}
86+
if ghErr.Response.StatusCode == http.StatusNotFound {
87+
log.Printf("[INFO] Removing user SSH key %s from state because it no longer exists in GitHub",
88+
d.Id())
89+
d.SetId("")
90+
return nil
91+
}
92+
}
93+
}
94+
95+
if err = d.Set("etag", resp.Header.Get("ETag")); err != nil {
96+
return err
97+
}
98+
if err = d.Set("title", key.GetTitle()); err != nil {
99+
return err
100+
}
101+
if err = d.Set("key", key.GetKey()); err != nil {
102+
return err
103+
}
104+
105+
return nil
106+
}
107+
108+
func resourceGithubUserSshSigningKeyDelete(d *schema.ResourceData, meta interface{}) error {
109+
client := meta.(*Owner).v3client
110+
111+
id, err := strconv.ParseInt(d.Id(), 10, 64)
112+
if err != nil {
113+
return unconvertibleIdErr(d.Id(), err)
114+
}
115+
ctx := context.WithValue(context.Background(), ctxId, d.Id())
116+
117+
_, err = client.Users.DeleteSSHSigningKey(ctx, id)
118+
return err
119+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package github
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"fmt"
7+
"regexp"
8+
"strings"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13+
"golang.org/x/crypto/ssh"
14+
)
15+
16+
func TestAccGithubUserSshSigningKey(t *testing.T) {
17+
18+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
19+
testKey := newTestSigningKey()
20+
21+
t.Run("creates and destroys a user SSH key without error", func(t *testing.T) {
22+
23+
config := fmt.Sprintf(`
24+
resource "github_user_ssh_signing_key" "test" {
25+
title = "tf-acc-test-%s"
26+
key = "%s"
27+
}
28+
`, randomID, testKey)
29+
30+
check := resource.ComposeTestCheckFunc(
31+
resource.TestMatchResourceAttr(
32+
"github_user_ssh_signing_key.test", "title",
33+
regexp.MustCompile(randomID),
34+
),
35+
resource.TestMatchResourceAttr(
36+
"github_user_ssh_signing_key.test", "key",
37+
regexp.MustCompile("^ssh-rsa "),
38+
),
39+
)
40+
41+
testCase := func(t *testing.T, mode string) {
42+
resource.Test(t, resource.TestCase{
43+
PreCheck: func() { skipUnlessMode(t, mode) },
44+
Providers: testAccProviders,
45+
Steps: []resource.TestStep{
46+
{
47+
Config: config,
48+
Check: check,
49+
},
50+
},
51+
})
52+
}
53+
54+
t.Run("with an anonymous account", func(t *testing.T) {
55+
t.Skip("anonymous account not supported for this operation")
56+
})
57+
58+
t.Run("with an individual account", func(t *testing.T) {
59+
testCase(t, individual)
60+
})
61+
62+
t.Run("with an organization account", func(t *testing.T) {
63+
testCase(t, organization)
64+
})
65+
66+
})
67+
68+
t.Run("imports an individual account SSH key without error", func(t *testing.T) {
69+
70+
config := fmt.Sprintf(`
71+
resource "github_user_ssh_signing_key" "test" {
72+
title = "tf-acc-test-%s"
73+
key = "%s"
74+
}
75+
`, randomID, testKey)
76+
77+
check := resource.ComposeTestCheckFunc(
78+
resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "title"),
79+
resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "key"),
80+
)
81+
82+
testCase := func(t *testing.T, mode string) {
83+
resource.Test(t, resource.TestCase{
84+
PreCheck: func() { skipUnlessMode(t, mode) },
85+
Providers: testAccProviders,
86+
Steps: []resource.TestStep{
87+
{
88+
Config: config,
89+
Check: check,
90+
},
91+
{
92+
ResourceName: "github_user_ssh_signing_key.test",
93+
ImportState: true,
94+
ImportStateVerify: true,
95+
},
96+
},
97+
})
98+
}
99+
100+
t.Run("with an anonymous account", func(t *testing.T) {
101+
t.Skip("anonymous account not supported for this operation")
102+
})
103+
104+
t.Run("with an individual account", func(t *testing.T) {
105+
testCase(t, individual)
106+
})
107+
108+
t.Run("with an organization account", func(t *testing.T) {
109+
testCase(t, organization)
110+
})
111+
112+
})
113+
}
114+
115+
func newTestSigningKey() string {
116+
privateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
117+
publicKey, _ := ssh.NewPublicKey(&privateKey.PublicKey)
118+
testKey := strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n")
119+
return testKey
120+
}

0 commit comments

Comments
 (0)