Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gh 271 GitHub secrets #362

Merged
merged 4 commits into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions github/data_source_github_actions_public_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package github

import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"log"
)

func dataSourceGithubActionsPublicKey() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubActionsPublicKeyRead,

Schema: map[string]*schema.Schema{
"repository": {
Type: schema.TypeString,
Required: true,
},
"key_id": {
Type: schema.TypeString,
Optional: true,
},
"key": {
Type: schema.TypeString,
Optional: true,
},
},
}
}

func dataSourceGithubActionsPublicKeyRead(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

repository := d.Get("repository").(string)
owner := meta.(*Organization).name
log.Printf("[INFO] Refreshing GitHub Actions Public Key from: %s/%s", owner, repository)

client := meta.(*Organization).client
ctx := context.Background()

publicKey, _, err := client.Actions.GetPublicKey(ctx, owner, repository)
if err != nil {
return err
}

d.SetId(publicKey.GetKeyID())
d.Set("key_id", publicKey.GetKeyID())
d.Set("key", publicKey.GetKey())

return nil
}
53 changes: 53 additions & 0 deletions github/data_source_github_actions_public_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package github

import (
"fmt"
"os"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccGithubActionsPublicKeyDataSource_noMatchReturnsError(t *testing.T) {
repo := "non-existent"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckGithubActionsPublicKeyDataSourceConfig(repo),
ExpectError: regexp.MustCompile(`Not Found`),
},
},
})
}

func TestAccCheckGithubActionsPublicKeyDataSource_existing(t *testing.T) {
repo := os.Getenv("GITHUB_TEMPLATE_REPOSITORY")
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckGithubActionsPublicKeyDataSourceConfig(repo),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("data.github_actions_public_key.test_pk", "key"),
resource.TestCheckResourceAttrSet("data.github_actions_public_key.test_pk", "key_id"),
),
},
},
})
}

func testAccCheckGithubActionsPublicKeyDataSourceConfig(repo string) string {
return fmt.Sprintf(`
data "github_actions_public_key" "test_pk" {
repository = "%s"
}
`, repo)
}
16 changes: 9 additions & 7 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func Provider() terraform.ResourceProvider {
},

ResourcesMap: map[string]*schema.Resource{
"github_actions_secret": resourceGithubActionsSecret(),
"github_branch_protection": resourceGithubBranchProtection(),
"github_issue_label": resourceGithubIssueLabel(),
"github_membership": resourceGithubMembership(),
Expand All @@ -68,13 +69,14 @@ func Provider() terraform.ResourceProvider {
},

DataSourcesMap: map[string]*schema.Resource{
"github_collaborators": dataSourceGithubCollaborators(),
"github_ip_ranges": dataSourceGithubIpRanges(),
"github_release": dataSourceGithubRelease(),
"github_repositories": dataSourceGithubRepositories(),
"github_repository": dataSourceGithubRepository(),
"github_team": dataSourceGithubTeam(),
"github_user": dataSourceGithubUser(),
"github_collaborators": dataSourceGithubCollaborators(),
"github_ip_ranges": dataSourceGithubIpRanges(),
"github_release": dataSourceGithubRelease(),
"github_repositories": dataSourceGithubRepositories(),
"github_repository": dataSourceGithubRepository(),
"github_team": dataSourceGithubTeam(),
"github_user": dataSourceGithubUser(),
"github_actions_public_key": dataSourceGithubActionsPublicKey(),
},
}

Expand Down
169 changes: 169 additions & 0 deletions github/resource_github_actions_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package github

import (
"context"
"encoding/base64"
"fmt"
"github.com/google/go-github/v29/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"golang.org/x/crypto/nacl/box"
"log"
)

func resourceGithubActionsSecret() *schema.Resource {
return &schema.Resource{
Create: resourceGithubActionsSecretCreateOrUpdate,
Read: resourceGithubActionsSecretRead,
Update: resourceGithubActionsSecretCreateOrUpdate,
Delete: resourceGithubActionsSecretDelete,

Schema: map[string]*schema.Schema{
"repository": {
Type: schema.TypeString,
Required: true,
},
"secret_name": {
Type: schema.TypeString,
Required: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add ForceNew: true here?

ForceNew: true,
},
"plaintext_value": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
"created_at": {
Type: schema.TypeString,
Computed: true,
},
"updated_at": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

client := meta.(*Organization).client
owner := meta.(*Organization).name
ctx := context.Background()

repo := d.Get("repository").(string)
secretName := d.Get("secret_name").(string)
plaintextValue := d.Get("plaintext_value").(string)

keyId, publicKey, err := getPublicKeyDetails(owner, repo, meta)
if err != nil {
return err
}

encryptedText, err := encryptPlaintext(plaintextValue, publicKey)
if err != nil {
return err
}

// Create an EncryptedSecret and encrypt the plaintext value into it
eSecret := &github.EncryptedSecret{
Name: secretName,
KeyID: keyId,
EncryptedValue: base64.StdEncoding.EncodeToString(encryptedText),
}

_, err = client.Actions.CreateOrUpdateSecret(ctx, owner, repo, eSecret)
if err != nil {
return err
}

Copy link
Contributor

@martinssipenko martinssipenko Feb 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should set the Id here using d.SetId(buildTwoPartID(&repo, &secretName)). Afterwards this func calls resourceGithubActionsSecretRead func, which instead should extract repo and secret name from the d.GetId() via parseTwoPartID func, I've given the example in this comment.

d.SetId(buildTwoPartID(&repo, &secretName))
return resourceGithubActionsSecretRead(d, meta)
}

func resourceGithubActionsSecretRead(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

client := meta.(*Organization).client
owner := meta.(*Organization).name
ctx := context.Background()

repoName, secretName, err := parseTwoPartID(d.Id(), "repository", "secret_name")
if err != nil {
return err
}

secret, _, err := client.Actions.GetSecret(ctx, owner, repoName, secretName)
if err != nil {
d.SetId("")
return err
}

Copy link
Contributor

@martinssipenko martinssipenko Feb 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come there are no d.Set() method calls? I think the secrets information could be stored in state. The UpdatedAt value from github.Secret could be used to determine if the value has changed in GitHub, for example, if UpdatedAt stored in state differs from value you got in GetSecret response, it means someone has updated the value outside of Terraform, and we probably want to sync it up (reapply value defined in Terraform).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't aware that the d.Set() functions were required to pass things to state. My understanding was that the existing d values passed in would get persisted.
I fully agree with the use of UpdatedAt - would you recommend saving the full state of the github.Secret ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would store the repo and name as par of Id, then store plaintext_value as it is and lastly store updated_at as computed getting the value from create/update operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a solid plan - I will also include created_at since we have it available and might as well make it available

d.Set("plaintext_value", d.Get("plaintext_value"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure but I think is not required, as it already should exist in state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also not sure on this point. I left it there such that it was obvious (and definite) it would be set. I don't see any issue with leaving this line in?

d.Set("updated_at", secret.UpdatedAt.Format("default"))
d.Set("created_at", secret.CreatedAt.Format("default"))

return nil
}

func resourceGithubActionsSecretDelete(d *schema.ResourceData, meta interface{}) error {
err := checkOrganization(meta)
if err != nil {
return err
}

client := meta.(*Organization).client
orgName := meta.(*Organization).name
ctx := context.WithValue(context.Background(), ctxId, d.Id())

repoName, secretName, err := parseTwoPartID(d.Id(), "repository", "secret_name")
if err != nil {
return err
}

log.Printf("[DEBUG] Deleting secret: %s", d.Id())
_, err = client.Actions.DeleteSecret(ctx, orgName, repoName, secretName)

return err
}

func getPublicKeyDetails(owner, repository string, meta interface{}) (keyId, pkValue string, err error) {
client := meta.(*Organization).client
ctx := context.Background()

publicKey, _, err := client.Actions.GetPublicKey(ctx, owner, repository)
if err != nil {
return keyId, pkValue, err
}

return publicKey.GetKeyID(), publicKey.GetKey(), err
}

func encryptPlaintext(plaintext, publicKeyB64 string) ([]byte, error) {
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyB64)
if err != nil {
return nil, err
}

var publicKeyBytes32 [32]byte
copiedLen := copy(publicKeyBytes32[:], publicKeyBytes)
if copiedLen == 0 {
return nil, fmt.Errorf("could not convert publicKey to bytes")
}

plaintextBytes := []byte(plaintext)
var encryptedBytes []byte

cipherText, err := box.SealAnonymous(encryptedBytes, plaintextBytes, &publicKeyBytes32, nil)
if err != nil {
return nil, err
}

return cipherText, nil
}
Loading