Skip to content

Commit

Permalink
add github_codespaces_organization_secret
Browse files Browse the repository at this point in the history
  • Loading branch information
KenSpur committed Jun 16, 2023
1 parent 09d813c commit d720679
Show file tree
Hide file tree
Showing 5 changed files with 560 additions and 0 deletions.
78 changes: 78 additions & 0 deletions github/data_source_github_codespaces_organization_secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package github

import (
"context"

"github.com/google/go-github/v53/github"

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

func dataSourceGithubDependabotOrganizationSecrets() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubDependabotOrganizationSecretsRead,

Schema: map[string]*schema.Schema{
"secrets": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Computed: true,
},
"visibility": {
Type: schema.TypeString,
Computed: true,
},
"created_at": {
Type: schema.TypeString,
Computed: true,
},
"updated_at": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
}
}

func dataSourceGithubDependabotOrganizationSecretsRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
owner := meta.(*Owner).name

options := github.ListOptions{
PerPage: 100,
}

var all_secrets []map[string]string
for {
secrets, resp, err := client.Dependabot.ListOrgSecrets(context.TODO(), owner, &options)
if err != nil {
return err
}
for _, secret := range secrets.Secrets {
new_secret := map[string]string{
"name": secret.Name,
"visibility": secret.Visibility,
"created_at": secret.CreatedAt.String(),
"updated_at": secret.UpdatedAt.String(),
}
all_secrets = append(all_secrets, new_secret)

}
if resp.NextPage == 0 {
break
}
options.Page = resp.NextPage
}

d.SetId(owner)
d.Set("secrets", all_secrets)

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

import (
"fmt"
"strings"
"testing"

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

func TestAccGithubCodespacesOrganizationSecretsDataSource(t *testing.T) {

t.Run("queries organization codespaces secrets from a repository", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

config := fmt.Sprintf(`
resource "github_codespaces_organization_secret" "test" {
secret_name = "org_dep_secret_1_%s"
plaintext_value = "foo"
visibility = "private"
}
`, randomID)

config2 := config + `
data "github_codespaces_organization_secrets" "test" {
}
`

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.github_codespaces_organization_secrets.test", "secrets.#", "1"),
resource.TestCheckResourceAttr("data.github_codespaces_organization_secrets.test", "secrets.0.name", strings.ToUpper(fmt.Sprintf("ORG_DEP_SECRET_1_%s", randomID))),
resource.TestCheckResourceAttr("data.github_codespaces_organization_secrets.test", "secrets.0.visibility", "private"),
resource.TestCheckResourceAttrSet("data.github_codespaces_organization_secrets.test", "secrets.0.created_at"),
resource.TestCheckResourceAttrSet("data.github_codespaces_organization_secrets.test", "secrets.0.updated_at"),
)

testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(),
},
{
Config: config2,
Check: check,
},
},
})
}

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})
})
}
2 changes: 2 additions & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func Provider() terraform.ResourceProvider {
"github_branch_default": resourceGithubBranchDefault(),
"github_branch_protection": resourceGithubBranchProtection(),
"github_branch_protection_v3": resourceGithubBranchProtectionV3(),
"github_codespaces_organization_secret": resourceGithubCodespacesOrganizationSecret(),
"github_codespaces_secret": resourceGithubCodespacesSecret(),
"github_dependabot_organization_secret": resourceGithubDependabotOrganizationSecret(),
"github_dependabot_organization_secret_repositories": resourceGithubDependabotOrganizationSecretRepositories(),
Expand Down Expand Up @@ -172,6 +173,7 @@ func Provider() terraform.ResourceProvider {
"github_branch": dataSourceGithubBranch(),
"github_branch_protection_rules": dataSourceGithubBranchProtectionRules(),
"github_collaborators": dataSourceGithubCollaborators(),
"github_codespaces_organization_secrets": dataSourceGithubCodespacesOrganizationSecrets(),
"github_codespaces_secrets": dataSourceGithubCodespacesSecrets(),
"github_dependabot_organization_public_key": dataSourceGithubDependabotOrganizationPublicKey(),
"github_dependabot_organization_secrets": dataSourceGithubDependabotOrganizationSecrets(),
Expand Down
235 changes: 235 additions & 0 deletions github/resource_github_codespaces_organization_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package github

import (
"context"
"encoding/base64"
"fmt"
"log"
"net/http"

"github.com/google/go-github/v53/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
)

func resourceGithubCodespacesOrganizationSecret() *schema.Resource {
return &schema.Resource{
Create: resourceGithubCodespacesOrganizationSecretCreateOrUpdate,
Read: resourceGithubCodespacesOrganizationSecretRead,
Update: resourceGithubCodespacesOrganizationSecretCreateOrUpdate,
Delete: resourceGithubCodespacesOrganizationSecretDelete,
Importer: &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
d.Set("secret_name", d.Id())
return []*schema.ResourceData{d}, nil
},
},

Schema: map[string]*schema.Schema{
"secret_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Name of the secret.",
ValidateFunc: validateSecretNameFunc,
},
"encrypted_value": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Sensitive: true,
ConflictsWith: []string{"plaintext_value"},
Description: "Encrypted value of the secret using the GitHub public key in Base64 format.",
ValidateFunc: validation.StringIsBase64,
},
"plaintext_value": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Sensitive: true,
Description: "Plaintext value of the secret to be encrypted.",
ConflictsWith: []string{"encrypted_value"},
},
"visibility": {
Type: schema.TypeString,
Required: true,
Description: "Configures the access that repositories have to the organization secret. Must be one of 'all', 'private' or 'selected'. 'selected_repository_ids' is required if set to 'selected'.",
ValidateFunc: validateValueFunc([]string{"all", "private", "selected"}),
ForceNew: true,
},
"selected_repository_ids": {
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeInt,
},
Set: schema.HashInt,
Optional: true,
Description: "An array of repository ids that can access the organization secret.",
},
"created_at": {
Type: schema.TypeString,
Computed: true,
Description: "Date of 'codespaces_secret' creation.",
},
"updated_at": {
Type: schema.TypeString,
Computed: true,
Description: "Date of 'codespaces_secret' update.",
},
},
}
}

func resourceGithubCodespacesOrganizationSecretCreateOrUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
owner := meta.(*Owner).name
ctx := context.Background()

secretName := d.Get("secret_name").(string)
plaintextValue := d.Get("plaintext_value").(string)
var encryptedValue string

visibility := d.Get("visibility").(string)
selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids")

if visibility != "selected" && hasSelectedRepositories {
return fmt.Errorf("cannot use selected_repository_ids without visibility being set to selected")
}

selectedRepositoryIDs := github.SelectedRepoIDs{}

if hasSelectedRepositories {
ids := selectedRepositories.(*schema.Set).List()

for _, id := range ids {
selectedRepositoryIDs = append(selectedRepositoryIDs, id.(int64))
}
}

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

if encryptedText, ok := d.GetOk("encrypted_value"); ok {
encryptedValue = encryptedText.(string)
} else {
encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey)
if err != nil {
return err
}
encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes)
}

// Create an EncryptedSecret and encrypt the plaintext value into it
eSecret := &github.EncryptedSecret{
Name: secretName,
KeyID: keyId,
Visibility: visibility,
SelectedRepositoryIDs: selectedRepositoryIDs,
EncryptedValue: encryptedValue,
}

_, err = client.Codespaces.CreateOrUpdateOrgSecret(ctx, owner, eSecret)
if err != nil {
return err
}

d.SetId(secretName)
return resourceGithubCodespacesOrganizationSecretRead(d, meta)
}

func resourceGithubCodespacesOrganizationSecretRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
owner := meta.(*Owner).name
ctx := context.Background()

secret, _, err := client.Codespaces.GetOrgSecret(ctx, owner, d.Id())
if err != nil {
if ghErr, ok := err.(*github.ErrorResponse); ok {
if ghErr.Response.StatusCode == http.StatusNotFound {
log.Printf("[WARN] Removing actions secret %s from state because it no longer exists in GitHub",
d.Id())
d.SetId("")
return nil
}
}
return err
}

d.Set("encrypted_value", d.Get("encrypted_value"))
d.Set("plaintext_value", d.Get("plaintext_value"))
d.Set("created_at", secret.CreatedAt.String())
d.Set("visibility", secret.Visibility)

selectedRepositoryIDs := []int64{}

if secret.Visibility == "selected" {
opt := &github.ListOptions{
PerPage: 30,
}
for {
results, resp, err := client.Codespaces.ListSelectedReposForOrgSecret(ctx, owner, d.Id(), opt)
if err != nil {
return err
}

for _, repo := range results.Repositories {
selectedRepositoryIDs = append(selectedRepositoryIDs, repo.GetID())
}

if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
}

d.Set("selected_repository_ids", selectedRepositoryIDs)

// This is a drift detection mechanism based on timestamps.
//
// If we do not currently store the "updated_at" field, it means we've only
// just created the resource and the value is most likely what we want it to
// be.
//
// If the resource is changed externally in the meantime then reading back
// the last update timestamp will return a result different than the
// timestamp we've persisted in the state. In that case, we can no longer
// trust that the value (which we don't see) is equal to what we've declared
// previously.
//
// The only solution to enforce consistency between is to mark the resource
// as deleted (unset the ID) in order to fix potential drift by recreating
// the resource.
if updatedAt, ok := d.GetOk("updated_at"); ok && updatedAt != secret.UpdatedAt.String() {
log.Printf("[WARN] The secret %s has been externally updated in GitHub", d.Id())
d.SetId("")
} else if !ok {
d.Set("updated_at", secret.UpdatedAt.String())
}

return nil
}

func resourceGithubCodespacesOrganizationSecretDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
orgName := meta.(*Owner).name
ctx := context.WithValue(context.Background(), ctxId, d.Id())

log.Printf("[DEBUG] Deleting secret: %s", d.Id())
_, err := client.Codespaces.DeleteOrgSecret(ctx, orgName, d.Id())
return err
}

func getCodespacesOrganizationPublicKeyDetails(owner string, meta interface{}) (keyId, pkValue string, err error) {
client := meta.(*Owner).v3client
ctx := context.Background()

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

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

0 comments on commit d720679

Please sign in to comment.