diff --git a/github/acc_test.go b/github/acc_test.go index eb7b5644ef..c12698c833 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -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 { @@ -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") diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..c87888e9d4 100644 --- a/github/provider.go +++ b/github/provider.go @@ -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(), diff --git a/github/resource_github_user_ssh_key.go b/github/resource_github_user_ssh_key.go index 969938ceca..3ed423ff1d 100644 --- a/github/resource_github_user_ssh_key.go +++ b/github/resource_github_user_ssh_key.go @@ -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, @@ -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 } diff --git a/github/resource_github_user_ssh_key_migration.go b/github/resource_github_user_ssh_key_migration.go new file mode 100644 index 0000000000..4d85c0befb --- /dev/null +++ b/github/resource_github_user_ssh_key_migration.go @@ -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 +} diff --git a/github/resource_github_user_ssh_key_migration_test.go b/github/resource_github_user_ssh_key_migration_test.go new file mode 100644 index 0000000000..91417fb713 --- /dev/null +++ b/github/resource_github_user_ssh_key_migration_test.go @@ -0,0 +1,49 @@ +package github + +import ( + "context" + "reflect" + "testing" +) + +func Test_resourceGithubUserSshKeyStateUpgradeV0(t *testing.T) { + t.Parallel() + + for _, d := range []struct { + testName string + rawState map[string]any + want map[string]any + shouldError bool + }{ + { + testName: "migrates_v0_to_v1", + rawState: map[string]any{ + "id": "123", + "title": "test-key", + "key": "test-key-data", + "url": "test-url", + }, + want: map[string]any{ + "id": "123", + "key_id": int64(123), + "title": "test-key", + "key": "test-key-data", + "url": "test-url", + }, + shouldError: false, + }, + } { + t.Run(d.testName, func(t *testing.T) { + t.Parallel() + + got, err := resourceGithubUserSshKeyStateUpgradeV0(context.Background(), d.rawState, nil) + if (err != nil) != d.shouldError { + t.Fatalf("unexpected error state") + } + + if !d.shouldError && !reflect.DeepEqual(got, d.want) { + t.Fatalf("got %+v, want %+v", got, d.want) + } + }) + } +} diff --git a/github/resource_github_user_ssh_key_test.go b/github/resource_github_user_ssh_key_test.go index f21ac61239..ad0b6336f8 100644 --- a/github/resource_github_user_ssh_key_test.go +++ b/github/resource_github_user_ssh_key_test.go @@ -4,40 +4,29 @@ import ( "crypto/rand" "crypto/rsa" "fmt" - "regexp" "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "golang.org/x/crypto/ssh" ) 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) - - 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/"), - ), - ) + `, name, testKey) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, @@ -45,7 +34,10 @@ func TestAccGithubUserSshKey(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, }, }, }) @@ -53,18 +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) - - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("github_user_ssh_key.test", "title"), - resource.TestCheckResourceAttrSet("github_user_ssh_key.test", "key"), - ) + `, name, testKey) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, @@ -72,7 +61,10 @@ func TestAccGithubUserSshKey(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, }, { ResourceName: "github_user_ssh_key.test", @@ -87,6 +79,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") } diff --git a/github/resource_github_user_ssh_signing_key.go b/github/resource_github_user_ssh_signing_key.go new file mode 100644 index 0000000000..b953c21345 --- /dev/null +++ b/github/resource_github_user_ssh_signing_key.go @@ -0,0 +1,142 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "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 resourceGithubUserSshSigningKey() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceGithubUserSshSigningKeyCreate, + ReadContext: resourceGithubUserSshSigningKeyRead, + DeleteContext: resourceGithubUserSshSigningKeyDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubUserSshSigningKeyImport, + }, + + Description: "Manages a SSH signing key for the authenticated user.", + + 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 signing key to add to your GitHub account.", + }, + "key_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The unique identifier of the SSH signing key.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceGithubUserSshSigningKeyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + title := d.Get("title").(string) + key := d.Get("key").(string) + + userKey, resp, err := client.Users.CreateSSHSigningKey(ctx, &github.Key{ + Title: new(title), + Key: new(key), + }) + if err != nil { + return diag.FromErr(err) + } + + 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 nil +} + +func resourceGithubUserSshSigningKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + keyID := int64(d.Get("key_id").(int)) + _, resp, err := client.Users.GetSSHSigningKey(ctx, keyID) + if err != nil { + return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "user SSH signing key (%d)", keyID)) + } + + // set computed fields + if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubUserSshSigningKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + keyID := int64(d.Get("key_id").(int)) + resp, err := client.Users.DeleteSSHSigningKey(ctx, keyID) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) + } + return nil +} + +func resourceGithubUserSshSigningKeyImport(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 signing key ID format: %w", err) + } + + key, _, err := client.Users.GetSSHSigningKey(ctx, keyID) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("SSH signing key with ID %d not found", keyID) + } + } + return nil, err + } + + if err = d.Set("key_id", key.GetID()); 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 + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_user_ssh_signing_key_test.go b/github/resource_github_user_ssh_signing_key_test.go new file mode 100644 index 0000000000..26ab4ab12f --- /dev/null +++ b/github/resource_github_user_ssh_signing_key_test.go @@ -0,0 +1,83 @@ +package github + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "golang.org/x/crypto/ssh" +) + +func TestAccGithubUserSshSigningKey(t *testing.T) { + t.Run("creates and destroys a user SSH signing key without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID) + testKey := newTestSigningKey() + + config := fmt.Sprintf(` + resource "github_user_ssh_signing_key" "test" { + title = "%[1]s" + key = "%[2]s" + } + `, name, testKey) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, + }, + }, + }) + }) + + t.Run("imports an individual account SSH signing key without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + name := fmt.Sprintf(`%s-%s`, testResourcePrefix, randomID) + testKey := newTestSigningKey() + + config := fmt.Sprintf(` + resource "github_user_ssh_signing_key" "test" { + title = "%[1]s" + key = "%[2]s" + } + `, name, testKey) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("title"), knownvalue.StringExact(name)), + statecheck.ExpectKnownValue("github_user_ssh_signing_key.test", tfjsonpath.New("key"), knownvalue.StringExact(testKey)), + }, + }, + { + ResourceName: "github_user_ssh_signing_key.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} + +func newTestSigningKey() string { + privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) + publicKey, _ := ssh.NewPublicKey(&privateKey.PublicKey) + return strings.TrimRight(string(ssh.MarshalAuthorizedKey(publicKey)), "\n") +} diff --git a/website/docs/r/user_ssh_key.html.markdown b/website/docs/r/user_ssh_key.html.markdown index 3827182bd1..500271a63b 100644 --- a/website/docs/r/user_ssh_key.html.markdown +++ b/website/docs/r/user_ssh_key.html.markdown @@ -24,7 +24,7 @@ resource "github_user_ssh_key" "example" { The following arguments are supported: -* `title` - (Required) A descriptive name for the new key. e.g. `Personal MacBook Air` +* `title` - (Required) A descriptive name for the new key. * `key` - (Required) The public SSH key to add to your GitHub account. ## Attributes Reference @@ -33,6 +33,8 @@ The following attributes are exported: * `id` - The ID of the SSH key * `url` - The URL of the SSH key +* `key_id` - The unique identifier of the SSH key. +* `etag` ## Import diff --git a/website/docs/r/user_ssh_signing_key.html.markdown b/website/docs/r/user_ssh_signing_key.html.markdown new file mode 100644 index 0000000000..045a4d0d19 --- /dev/null +++ b/website/docs/r/user_ssh_signing_key.html.markdown @@ -0,0 +1,44 @@ +--- +layout: "github" +page_title: "GitHub: github_user_ssh_signing_key" +description: |- + Provides a GitHub user's SSH signing key resource. +--- + +# github_user_ssh_signing_key + +Provides a GitHub user's SSH signing key resource. + +This resource allows you to add/remove SSH signing keys from your user account. + +## Example Usage + +```hcl +resource "github_user_ssh_signing_key" "example" { + title = "example title" + key = file("~/.ssh/id_rsa.pub") +} +``` + +## Argument Reference + +The following arguments are supported: + +* `title` - (Required) A descriptive name for the new key. +* `key` - (Required) The public SSH signing key to add to your GitHub account. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the SSH signing key +* `key_id` - The unique identifier of the SSH signing key. +* `etag` + +## Import + +SSH signing keys can be imported using their ID e.g. + +``` +$ terraform import github_user_ssh_signing_key.example 1234567 +``` diff --git a/website/github.erb b/website/github.erb index 997536b42f..2285f531be 100644 --- a/website/github.erb +++ b/website/github.erb @@ -451,6 +451,9 @@
  • github_user_ssh_key
  • +
  • + github_user_ssh_signing_key +