Skip to content

Commit a74c9d4

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 a74c9d4

6 files changed

Lines changed: 295 additions & 0 deletions

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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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 any) 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+
87+
key, resp, err := client.Users.GetSSHSigningKey(ctx, id)
88+
if err != nil {
89+
if ghErr, ok := err.(*github.ErrorResponse); ok {
90+
if ghErr.Response.StatusCode == http.StatusNotModified {
91+
return nil
92+
}
93+
if ghErr.Response.StatusCode == http.StatusNotFound {
94+
tflog.Info(ctx, fmt.Sprintf("Removing user SSH key %s from state because it no longer exists in GitHub", d.Id()), map[string]any{
95+
"ssh_signing_key_id": d.Id(),
96+
})
97+
d.SetId("")
98+
return nil
99+
}
100+
}
101+
}
102+
103+
if err = d.Set("etag", resp.Header.Get("ETag")); err != nil {
104+
return diag.FromErr(err)
105+
}
106+
if err = d.Set("title", key.GetTitle()); err != nil {
107+
return diag.FromErr(err)
108+
}
109+
if err = d.Set("key", key.GetKey()); err != nil {
110+
return diag.FromErr(err)
111+
}
112+
113+
return nil
114+
}
115+
116+
func resourceGithubUserSshSigningKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
117+
client := meta.(*Owner).v3client
118+
119+
id, err := strconv.ParseInt(d.Id(), 10, 64)
120+
if err != nil {
121+
return diag.Errorf("failed to convert ID %s: %v", d.Id(), err)
122+
}
123+
124+
resp, err := client.Users.DeleteSSHSigningKey(ctx, id)
125+
if resp.StatusCode == http.StatusNotFound {
126+
return nil
127+
}
128+
return diag.FromErr(err)
129+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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("github_user_ssh_signing_key.test", "title", regexp.MustCompile(randomID)),
31+
resource.TestMatchResourceAttr("github_user_ssh_signing_key.test", "key", regexp.MustCompile("^ssh-rsa ")),
32+
)
33+
34+
resource.Test(t, resource.TestCase{
35+
PreCheck: func() { skipUnauthenticated(t) },
36+
ProviderFactories: providerFactories,
37+
Steps: []resource.TestStep{
38+
{
39+
Config: config,
40+
Check: check,
41+
},
42+
},
43+
})
44+
})
45+
46+
t.Run("imports an individual account SSH signing key without error", func(t *testing.T) {
47+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
48+
name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID)
49+
testKey := newTestSigningKey()
50+
51+
config := fmt.Sprintf(`
52+
resource "github_user_ssh_signing_key" "test" {
53+
title = "%[1]s"
54+
key = "%[2]s"
55+
}
56+
`, name, testKey)
57+
58+
check := resource.ComposeTestCheckFunc(
59+
resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "title"),
60+
resource.TestCheckResourceAttrSet("github_user_ssh_signing_key.test", "key"),
61+
)
62+
63+
resource.Test(t, resource.TestCase{
64+
PreCheck: func() { skipUnauthenticated(t) },
65+
ProviderFactories: providerFactories,
66+
Steps: []resource.TestStep{
67+
{
68+
Config: config,
69+
Check: check,
70+
},
71+
{
72+
ResourceName: "github_user_ssh_signing_key.test",
73+
ImportState: true,
74+
ImportStateVerify: true,
75+
},
76+
},
77+
})
78+
})
79+
}
80+
81+
func newTestSigningKey() string {
82+
privateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
83+
publicKey, _ := ssh.NewPublicKey(&privateKey.PublicKey)
84+
return strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n")
85+
}
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)