Skip to content

Commit 07dd337

Browse files
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
1 parent 4c95ca9 commit 07dd337

File tree

6 files changed

+307
-0
lines changed

6 files changed

+307
-0
lines changed

github/acc_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ func configureSweepers() {
204204
Name: "teams",
205205
F: sweepTeams,
206206
})
207+
208+
resource.AddTestSweepers("user_ssh_signing_keys", &resource.Sweeper{
209+
Name: "user_ssh_signing_keys",
210+
F: sweepUserSSHSigningKeys,
211+
})
207212
}
208213

209214
func sweepTeams(_ string) error {
@@ -276,6 +281,36 @@ func sweepRepositories(_ string) error {
276281
return nil
277282
}
278283

284+
func sweepUserSSHSigningKeys(_ string) error {
285+
fmt.Println("sweeping user SSH signing keys")
286+
287+
meta, err := getTestMeta()
288+
if err != nil {
289+
return fmt.Errorf("could not get test meta for sweeper: %w", err)
290+
}
291+
292+
client := meta.v3client
293+
owner := meta.name
294+
ctx := context.Background()
295+
296+
keys, _, err := client.Users.ListSSHSigningKeys(ctx, owner, nil)
297+
if err != nil {
298+
return err
299+
}
300+
301+
for _, k := range keys {
302+
if title := k.GetTitle(); strings.HasPrefix(title, testResourcePrefix) {
303+
fmt.Printf("destroying user SSH signing key %s\n", title)
304+
305+
if _, err := client.Users.DeleteSSHSigningKey(ctx, k.GetID()); err != nil {
306+
return err
307+
}
308+
}
309+
}
310+
311+
return nil
312+
}
313+
279314
func skipUnauthenticated(t *testing.T) {
280315
if testAccConf.authMode == anonymous {
281316
t.Skip("Skipping as test mode not authenticated")

github/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ func Provider() *schema.Provider {
212212
"github_user_gpg_key": resourceGithubUserGpgKey(),
213213
"github_user_invitation_accepter": resourceGithubUserInvitationAccepter(),
214214
"github_user_ssh_key": resourceGithubUserSshKey(),
215+
"github_user_ssh_signing_key": resourceGithubUserSshSigningKey(),
215216
"github_enterprise_organization": resourceGithubEnterpriseOrganization(),
216217
"github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(),
217218
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/google/go-github/v81/github"
11+
"github.com/hashicorp/terraform-plugin-log/tflog"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14+
)
15+
16+
func resourceGithubUserSshSigningKey() *schema.Resource {
17+
return &schema.Resource{
18+
CreateContext: resourceGithubUserSshSigningKeyCreate,
19+
ReadContext: resourceGithubUserSshSigningKeyRead,
20+
DeleteContext: resourceGithubUserSshSigningKeyDelete,
21+
Importer: &schema.ResourceImporter{
22+
StateContext: schema.ImportStatePassthroughContext,
23+
},
24+
25+
Schema: map[string]*schema.Schema{
26+
"title": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
ForceNew: true,
30+
Description: "A descriptive name for the new key.",
31+
},
32+
"key": {
33+
Type: schema.TypeString,
34+
Required: true,
35+
ForceNew: true,
36+
Description: "The public SSH key to add to your GitHub account.",
37+
DiffSuppressFunc: func(k, oldV, newV string, d *schema.ResourceData) bool {
38+
newTrimmed := strings.TrimSpace(newV)
39+
return oldV == newTrimmed
40+
},
41+
},
42+
"etag": {
43+
Type: schema.TypeString,
44+
Computed: true,
45+
},
46+
},
47+
}
48+
}
49+
50+
func resourceGithubUserSshSigningKeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
51+
client := meta.(*Owner).v3client
52+
53+
title := d.Get("title").(string)
54+
key := d.Get("key").(string)
55+
56+
userKey, resp, err := client.Users.CreateSSHSigningKey(ctx, &github.Key{
57+
Title: github.Ptr(title),
58+
Key: github.Ptr(key),
59+
})
60+
if err != nil {
61+
return diag.FromErr(err)
62+
}
63+
64+
d.SetId(strconv.FormatInt(*userKey.ID, 10))
65+
66+
if err = d.Set("etag", resp.Header.Get("ETag")); err != nil {
67+
return diag.FromErr(err)
68+
}
69+
if err = d.Set("title", userKey.GetTitle()); err != nil {
70+
return diag.FromErr(err)
71+
}
72+
if err = d.Set("key", userKey.GetKey()); err != nil {
73+
return diag.FromErr(err)
74+
}
75+
76+
return nil
77+
}
78+
79+
func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
80+
client := meta.(*Owner).v3client
81+
82+
id, err := strconv.ParseInt(d.Id(), 10, 64)
83+
if err != nil {
84+
return diag.Errorf("failed to convert ID %s: %v", d.Id(), err)
85+
}
86+
if !d.IsNewResource() {
87+
ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string))
88+
}
89+
90+
key, resp, err := client.Users.GetSSHSigningKey(ctx, id)
91+
if err != nil {
92+
if ghErr, ok := err.(*github.ErrorResponse); ok {
93+
if ghErr.Response.StatusCode == http.StatusNotModified {
94+
return nil
95+
}
96+
if ghErr.Response.StatusCode == http.StatusNotFound {
97+
tflog.Info(ctx, fmt.Sprintf("Removing user SSH key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{
98+
"ssh_signing_key_id": d.Id(),
99+
})
100+
d.SetId("")
101+
return nil
102+
}
103+
}
104+
}
105+
106+
if err = d.Set("etag", resp.Header.Get("ETag")); err != nil {
107+
return diag.FromErr(err)
108+
}
109+
if err = d.Set("title", key.GetTitle()); err != nil {
110+
return diag.FromErr(err)
111+
}
112+
if err = d.Set("key", key.GetKey()); err != nil {
113+
return diag.FromErr(err)
114+
}
115+
116+
return nil
117+
}
118+
119+
func resourceGithubUserSshSigningKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
120+
client := meta.(*Owner).v3client
121+
122+
id, err := strconv.ParseInt(d.Id(), 10, 64)
123+
if err != nil {
124+
return diag.Errorf("failed to convert ID %s: %v", d.Id(), err)
125+
}
126+
ctx = context.WithValue(ctx, ctxId, d.Id())
127+
128+
resp, err := client.Users.DeleteSSHSigningKey(ctx, id)
129+
if resp.StatusCode == http.StatusNotFound {
130+
return nil
131+
}
132+
return diag.FromErr(err)
133+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
t.Run("creates and destroys a user SSH signing key without error", func(t *testing.T) {
18+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
19+
name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID)
20+
testKey := newTestSigningKey()
21+
22+
config := fmt.Sprintf(`
23+
resource "github_user_ssh_signing_key" "test" {
24+
title = "%[1]s"
25+
key = "%[2]s"
26+
}
27+
`, name, testKey)
28+
29+
check := resource.ComposeTestCheckFunc(
30+
resource.TestMatchResourceAttr(
31+
"github_user_ssh_signing_key.test",
32+
"title",
33+
regexp.MustCompile(randomID),
34+
),
35+
resource.TestMatchResourceAttr(
36+
"github_user_ssh_signing_key.test",
37+
"key",
38+
regexp.MustCompile("^ssh-rsa "),
39+
),
40+
)
41+
42+
resource.Test(t, resource.TestCase{
43+
PreCheck: func() { skipUnauthenticated(t) },
44+
ProviderFactories: providerFactories,
45+
Steps: []resource.TestStep{
46+
{
47+
Config: config,
48+
Check: check,
49+
},
50+
},
51+
})
52+
})
53+
54+
t.Run("imports an individual account SSH signing key without error", func(t *testing.T) {
55+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
56+
name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID)
57+
testKey := newTestSigningKey()
58+
59+
config := fmt.Sprintf(`
60+
resource "github_user_ssh_signing_key" "test" {
61+
title = "%[1]s"
62+
key = "%[2]s"
63+
}
64+
`, name, testKey)
65+
66+
check := resource.ComposeTestCheckFunc(
67+
resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "title"),
68+
resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "key"),
69+
)
70+
71+
resource.Test(t, resource.TestCase{
72+
PreCheck: func() { skipUnauthenticated(t) },
73+
ProviderFactories: providerFactories,
74+
Steps: []resource.TestStep{
75+
{
76+
Config: config,
77+
Check: check,
78+
},
79+
{
80+
ResourceName: "github_user_ssh_signing_key.test",
81+
ImportState: true,
82+
ImportStateVerify: true,
83+
},
84+
},
85+
})
86+
})
87+
}
88+
89+
func newTestSigningKey() string {
90+
privateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
91+
publicKey, _ := ssh.NewPublicKey(&privateKey.PublicKey)
92+
return strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n")
93+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
layout: "github"
3+
page_title: "GitHub: github_user_ssh_signing_key"
4+
description: |-
5+
Provides a GitHub user's SSH signing key resource.
6+
---
7+
8+
# github_user_ssh_signing_key
9+
10+
Provides a GitHub user's SSH signing key resource.
11+
12+
This resource allows you to add/remove SSH signing keys from your user account.
13+
14+
## Example Usage
15+
16+
```hcl
17+
resource "github_user_ssh_signing_key" "example" {
18+
title = "example title"
19+
key = file("~/.ssh/id_rsa.pub")
20+
}
21+
```
22+
23+
## Argument Reference
24+
25+
The following arguments are supported:
26+
27+
* `title` - (Required) A descriptive name for the new key. e.g. `Personal MacBook Air`
28+
* `key` - (Required) The public SSH signing key to add to your GitHub account.
29+
30+
## Attributes Reference
31+
32+
The following attributes are exported:
33+
34+
* `id` - The ID of the SSH signing key
35+
36+
## Import
37+
38+
SSH signing keys can be imported using their ID e.g.
39+
40+
```
41+
$ terraform import github_user_ssh_signing_key.example 1234567
42+
```

website/github.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,9 @@
451451
<li>
452452
<a href="/docs/providers/github/r/user_ssh_key.html">github_user_ssh_key</a>
453453
</li>
454+
<li>
455+
<a href="/docs/providers/github/r/user_ssh_signing_key.html">github_user_ssh_signing_key</a>
456+
</li>
454457
</ul>
455458
</li>
456459
</ul>

0 commit comments

Comments
 (0)