diff --git a/github/provider.go b/github/provider.go index c4457eabca..bce2e25b67 100644 --- a/github/provider.go +++ b/github/provider.go @@ -51,6 +51,7 @@ func Provider() terraform.ResourceProvider { "github_team": resourceGithubTeam(), "github_team_membership": resourceGithubTeamMembership(), "github_team_repository": resourceGithubTeamRepository(), + "github_user_gpg_key": resourceGithubUserGpgKey(), "github_user_ssh_key": resourceGithubUserSshKey(), }, diff --git a/github/resource_github_user_gpg_key.go b/github/resource_github_user_gpg_key.go new file mode 100644 index 0000000000..ee09053a6b --- /dev/null +++ b/github/resource_github_user_gpg_key.go @@ -0,0 +1,77 @@ +package github + +import ( + "context" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceGithubUserGpgKey() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubUserGpgKeyCreate, + Read: resourceGithubUserGpgKeyRead, + Delete: resourceGithubUserGpgKeyDelete, + + Schema: map[string]*schema.Schema{ + "armored_public_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "key_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceGithubUserGpgKeyCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + + pubKey := d.Get("armored_public_key").(string) + + key, _, err := client.Users.CreateGPGKey(context.TODO(), pubKey) + if err != nil { + return err + } + + d.SetId(strconv.FormatInt(*key.ID, 10)) + + return resourceGithubUserGpgKeyRead(d, meta) +} + +func resourceGithubUserGpgKeyRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + + id, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + + key, _, err := client.Users.GetGPGKey(context.TODO(), id) + if err != nil { + log.Printf("[WARN] GitHub User GPG Key (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("key_id", key.KeyID) + + return nil +} + +func resourceGithubUserGpgKeyDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + + id, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + + _, err = client.Users.DeleteGPGKey(context.TODO(), id) + + return err +} diff --git a/github/resource_github_user_gpg_key_test.go b/github/resource_github_user_gpg_key_test.go new file mode 100644 index 0000000000..49fbeed461 --- /dev/null +++ b/github/resource_github_user_gpg_key_test.go @@ -0,0 +1,91 @@ +package github + +import ( + "context" + "fmt" + "path/filepath" + "regexp" + "strconv" + "testing" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGithubUserGpgKey_basic(t *testing.T) { + var key github.GPGKey + keyRe := regexp.MustCompile("^-----BEGIN PGP PUBLIC KEY BLOCK-----") + pubKeyPath := filepath.Join("test-fixtures", "gpg-pubkey.asc") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubUserGpgKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubUserGpgKeyConfig(pubKeyPath), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubUserGpgKeyExists("github_user_gpg_key.test", &key), + resource.TestMatchResourceAttr("github_user_gpg_key.test", "armored_public_key", keyRe), + resource.TestCheckResourceAttr("github_user_gpg_key.test", "key_id", "AC541D2D1709CD33"), + ), + }, + }, + }) +} + +func testAccCheckGithubUserGpgKeyExists(n string, key *github.GPGKey) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + id, err := strconv.ParseInt(rs.Primary.ID, 10, 64) + if err != nil { + return unconvertibleIdErr(rs.Primary.ID, err) + } + + org := testAccProvider.Meta().(*Organization) + receivedKey, _, err := org.client.Users.GetGPGKey(context.TODO(), id) + if err != nil { + return err + } + *key = *receivedKey + return nil + } +} + +func testAccCheckGithubUserGpgKeyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Organization).client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_user_gpg_key" { + continue + } + + id, err := strconv.ParseInt(rs.Primary.ID, 10, 64) + if err != nil { + return unconvertibleIdErr(rs.Primary.ID, err) + } + + _, resp, err := conn.Users.GetGPGKey(context.TODO(), id) + if err == nil { + return fmt.Errorf("GPG key %s still exists", rs.Primary.ID) + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +func testAccGithubUserGpgKeyConfig(pubKeyPath string) string { + return fmt.Sprintf(` +resource "github_user_gpg_key" "test" { + armored_public_key = "${file("%s")}" +} +`, pubKeyPath) +} diff --git a/github/test-fixtures/gpg-pubkey.asc b/github/test-fixtures/gpg-pubkey.asc new file mode 100644 index 0000000000..4d47c03438 --- /dev/null +++ b/github/test-fixtures/gpg-pubkey.asc @@ -0,0 +1,53 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=FSt9 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/website/docs/r/user_gpg_key.html.markdown b/website/docs/r/user_gpg_key.html.markdown new file mode 100644 index 0000000000..0c9d627c3b --- /dev/null +++ b/website/docs/r/user_gpg_key.html.markdown @@ -0,0 +1,40 @@ +--- +layout: "github" +page_title: "GitHub: github_user_gpg_key" +sidebar_current: "docs-github-resource-user-gpg-key" +description: |- + Provides a GitHub user's GPG key resource. +--- + +# github_user_gpg_key + +Provides a GitHub user's GPG key resource. + +This resource allows you to add/remove GPG keys from your user account. + +## Example Usage + +```hcl +resource "github_user_gpg_key" "example" { + armored_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `armored_public_key` - (Required) Your pulic GPG key, generated in ASCII-armored format. + See [Generating a new GPG key](https://help.github.com/articles/generating-a-new-gpg-key/) for help on creating a GPG key. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The GitHub ID of the GPG key, e.g. `401586` +* `key_id` - The key ID of the GPG key, e.g. `3262EFF25BA0D270` + +## Import + +GPG keys are not importable due to the fact that [API](https://developer.github.com/v3/users/gpg_keys/#gpg-keys) +does not return previously uploaded GPG key. diff --git a/website/github.erb b/website/github.erb index c3ed8814bf..3a1a75e74b 100644 --- a/website/github.erb +++ b/website/github.erb @@ -34,9 +34,15 @@