diff --git a/github/provider.go b/github/provider.go index 9e9a058892..2edcca6079 100644 --- a/github/provider.go +++ b/github/provider.go @@ -139,9 +139,11 @@ func Provider() *schema.Provider { "github_actions_organization_oidc_subject_claim_customization_template": resourceGithubActionsOrganizationOIDCSubjectClaimCustomizationTemplate(), "github_actions_organization_permissions": resourceGithubActionsOrganizationPermissions(), "github_actions_organization_secret": resourceGithubActionsOrganizationSecret(), - "github_actions_organization_variable": resourceGithubActionsOrganizationVariable(), "github_actions_organization_secret_repositories": resourceGithubActionsOrganizationSecretRepositories(), "github_actions_organization_secret_repository": resourceGithubActionsOrganizationSecretRepository(), + "github_actions_organization_variable": resourceGithubActionsOrganizationVariable(), + "github_actions_organization_variable_repositories": resourceGithubActionsOrganizationVariableRepositories(), + "github_actions_organization_variable_repository": resourceGithubActionsOrganizationVariableRepository(), "github_actions_repository_access_level": resourceGithubActionsRepositoryAccessLevel(), "github_actions_repository_oidc_subject_claim_customization_template": resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate(), "github_actions_repository_permissions": resourceGithubActionsRepositoryPermissions(), @@ -161,6 +163,7 @@ func Provider() *schema.Provider { "github_codespaces_user_secret": resourceGithubCodespacesUserSecret(), "github_dependabot_organization_secret": resourceGithubDependabotOrganizationSecret(), "github_dependabot_organization_secret_repositories": resourceGithubDependabotOrganizationSecretRepositories(), + "github_dependabot_organization_secret_repository": resourceGithubDependabotOrganizationSecretRepository(), "github_dependabot_secret": resourceGithubDependabotSecret(), "github_emu_group_mapping": resourceGithubEMUGroupMapping(), "github_issue": resourceGithubIssue(), diff --git a/github/resource_github_actions_organization_secret.go b/github/resource_github_actions_organization_secret.go index 22960944e9..3600308ad1 100644 --- a/github/resource_github_actions_organization_secret.go +++ b/github/resource_github_actions_organization_secret.go @@ -69,6 +69,7 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource { }, Optional: true, Description: "An array of repository IDs that can access the organization secret.", + Deprecated: "This field is deprecated and will be removed in a future release. Please use the `github_actions_organization_secret_repositories` or `github_actions_organization_secret_repository` resources to manage repository access to organization secrets.", }, "created_at": { Type: schema.TypeString, @@ -225,32 +226,34 @@ func resourceGithubActionsOrganizationSecretRead(ctx context.Context, d *schema. return diag.FromErr(err) } - repoIDs := []int64{} if secret.Visibility == "selected" { - opt := &github.ListOptions{ - PerPage: maxPerPage, - } - for { - results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) - if err != nil { - return diag.FromErr(err) + if _, ok := d.GetOk("selected_repository_ids"); ok { + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, } - - for _, repo := range results.Repositories { - repoIDs = append(repoIDs, repo.GetID()) + for { + results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) + if err != nil { + return diag.FromErr(err) + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage } - if resp.NextPage == 0 { - break + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return diag.FromErr(err) } - opt.Page = resp.NextPage } } - if err := d.Set("selected_repository_ids", repoIDs); err != nil { - return diag.FromErr(err) - } - return nil } diff --git a/github/resource_github_actions_organization_secret_repositories.go b/github/resource_github_actions_organization_secret_repositories.go index 1610e19094..bbeb3f6e4f 100644 --- a/github/resource_github_actions_organization_secret_repositories.go +++ b/github/resource_github_actions_organization_secret_repositories.go @@ -4,91 +4,91 @@ import ( "context" "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubActionsOrganizationSecretRepositories() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsOrganizationSecretRepositoriesCreateOrUpdate, - Read: resourceGithubActionsOrganizationSecretRepositoriesRead, - Update: resourceGithubActionsOrganizationSecretRepositoriesCreateOrUpdate, - Delete: resourceGithubActionsOrganizationSecretRepositoriesDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ "secret_name": { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Name of the existing secret.", ValidateDiagFunc: validateSecretNameFunc, + Description: "Name of the existing secret.", }, "selected_repository_ids": { Type: schema.TypeSet, + Set: schema.HashInt, Elem: &schema.Schema{ Type: schema.TypeInt, }, - Set: schema.HashInt, Required: true, Description: "An array of repository ids that can access the organization secret.", }, }, + + CreateContext: resourceGithubActionsOrganizationSecretRepositoriesCreateOrUpdate, + ReadContext: resourceGithubActionsOrganizationSecretRepositoriesRead, + UpdateContext: resourceGithubActionsOrganizationSecretRepositoriesCreateOrUpdate, + DeleteContext: resourceGithubActionsOrganizationSecretRepositoriesDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubActionsOrganizationSecretRepositoriesImport, + }, } } -func resourceGithubActionsOrganizationSecretRepositoriesCreateOrUpdate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() - - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubActionsOrganizationSecretRepositoriesCreateOrUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - secretName := d.Get("secret_name").(string) - selectedRepositories := d.Get("selected_repository_ids") + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - selectedRepositoryIDs := []int64{} + secretName := d.Get("secret_name").(string) + repoIDs := []int64{} - ids := selectedRepositories.(*schema.Set).List() + ids := d.Get("selected_repository_ids").(*schema.Set).List() for _, id := range ids { - selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int))) + repoIDs = append(repoIDs, int64(id.(int))) } - _, err = client.Actions.SetSelectedReposForOrgSecret(ctx, owner, secretName, selectedRepositoryIDs) + _, err := client.Actions.SetSelectedReposForOrgSecret(ctx, owner, secretName, repoIDs) if err != nil { - return err + return diag.FromErr(err) } d.SetId(secretName) - return resourceGithubActionsOrganizationSecretRepositoriesRead(d, meta) -} -func resourceGithubActionsOrganizationSecretRepositoriesRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() + return nil +} - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubActionsOrganizationSecretRepositoriesRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - selectedRepositoryIDs := []int64{} + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Get("secret_name").(string) + + repoIDs := []int64{} opt := &github.ListOptions{ PerPage: maxPerPage, } for { - results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, d.Id(), opt) + results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) if err != nil { - return err + return diag.FromErr(err) } for _, repo := range results.Repositories { - selectedRepositoryIDs = append(selectedRepositoryIDs, repo.GetID()) + repoIDs = append(repoIDs, repo.GetID()) } if resp.NextPage == 0 { @@ -97,28 +97,64 @@ func resourceGithubActionsOrganizationSecretRepositoriesRead(d *schema.ResourceD opt.Page = resp.NextPage } - if err = d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { - return err + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return diag.FromErr(err) } return nil } -func resourceGithubActionsOrganizationSecretRepositoriesDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubActionsOrganizationSecretRepositoriesDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - selectedRepositoryIDs := []int64{} - _, err = client.Actions.SetSelectedReposForOrgSecret(ctx, owner, d.Id(), selectedRepositoryIDs) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + _, err := client.Actions.SetSelectedReposForOrgSecret(ctx, owner, d.Id(), []int64{}) if err != nil { - return err + return diag.FromErr(err) } return nil } + +func resourceGithubActionsOrganizationSecretRepositoriesImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Id() + + if err := d.Set("secret_name", secretName); err != nil { + return nil, err + } + + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, + } + for { + results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) + if err != nil { + return nil, err + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_actions_organization_secret_repositories_test.go b/github/resource_github_actions_organization_secret_repositories_test.go index dd37037d5d..508ef5dd49 100644 --- a/github/resource_github_actions_organization_secret_repositories_test.go +++ b/github/resource_github_actions_organization_secret_repositories_test.go @@ -1,6 +1,7 @@ package github import ( + "encoding/base64" "fmt" "testing" @@ -9,45 +10,38 @@ import ( ) func TestAccGithubActionsOrganizationSecretRepositories(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName1 := fmt.Sprintf("%srepo-act-org-secret-%s-1", testResourcePrefix, randomID) - repoName2 := fmt.Sprintf("%srepo-act-org-secret-%s-2", testResourcePrefix, randomID) - - t.Run("set repository allowlist for a organization secret", func(t *testing.T) { - if len(testAccConf.testOrgSecretName) == 0 { - t.Skipf("'GH_TEST_ORG_SECRET_NAME' environment variable is missing") - } + t.Run("create", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + secretName := fmt.Sprintf("test_%s", randomID) + secretValue := base64.StdEncoding.EncodeToString([]byte("foo")) + repoName0 := fmt.Sprintf("%s%s-0", testResourcePrefix, randomID) + repoName1 := fmt.Sprintf("%s%s-1", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_repository" "test_repo_1" { - name = "%s" - visibility = "internal" - vulnerability_alerts = "true" - } - - resource "github_repository" "test_repo_2" { - name = "%s" - visibility = "internal" - vulnerability_alerts = "true" - } - - resource "github_actions_organization_secret_repositories" "org_secret_repos" { - secret_name = "%s" - selected_repository_ids = [ - github_repository.test_repo_1.repo_id, - github_repository.test_repo_2.repo_id - ] - } - `, repoName1, repoName2, testAccConf.testOrgSecretName) - - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet( - "github_actions_organization_secret_repositories.org_secret_repos", "secret_name", - ), - resource.TestCheckResourceAttr( - "github_actions_organization_secret_repositories.org_secret_repos", "selected_repository_ids.#", "2", - ), - ) +resource "github_actions_organization_secret" "test" { + secret_name = "%s" + encrypted_value = "%s" + visibility = "selected" +} + +resource "github_repository" "test_0" { + name = "%s" + visibility = "public" +} + +resource "github_repository" "test_1" { + name = "%s" + visibility = "public" +} + +resource "github_actions_organization_secret_repositories" "test" { + secret_name = github_actions_organization_secret.test.secret_name + selected_repository_ids = [ + github_repository.test_0.repo_id, + github_repository.test_1.repo_id + ] +} +`, secretName, secretValue, repoName0, repoName1) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -55,7 +49,10 @@ func TestAccGithubActionsOrganizationSecretRepositories(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: check, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_actions_organization_secret_repositories.test", "secret_name", "github_actions_organization_secret.test", "secret_name"), + resource.TestCheckResourceAttr("github_actions_organization_secret_repositories.test", "selected_repository_ids.#", "2"), + ), }, }, }) diff --git a/github/resource_github_actions_organization_secret_repository.go b/github/resource_github_actions_organization_secret_repository.go index 32f370b599..b73c839882 100644 --- a/github/resource_github_actions_organization_secret_repository.go +++ b/github/resource_github_actions_organization_secret_repository.go @@ -6,25 +6,19 @@ import ( "strconv" "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubActionsOrganizationSecretRepository() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsOrganizationSecretRepositoryCreate, - Read: resourceGithubActionsOrganizationSecretRepositoryRead, - Delete: resourceGithubActionsOrganizationSecretRepositoryDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ "secret_name": { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Name of the existing secret.", ValidateDiagFunc: validateSecretNameFunc, + Description: "Name of the existing secret.", }, "repository_id": { Type: schema.TypeInt, @@ -33,74 +27,70 @@ func resourceGithubActionsOrganizationSecretRepository() *schema.Resource { Description: "The repository ID that can access the organization secret.", }, }, + + CreateContext: resourceGithubActionsOrganizationSecretRepositoryCreate, + ReadContext: resourceGithubActionsOrganizationSecretRepositoryRead, + DeleteContext: resourceGithubActionsOrganizationSecretRepositoryDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubActionsOrganizationSecretRepositoryImport, + }, } } -func resourceGithubActionsOrganizationSecretRepositoryCreate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() - - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubActionsOrganizationSecretRepositoryCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - repositoryID := d.Get("repository_id").(int) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + secretName := d.Get("secret_name").(string) + repoID := d.Get("repository_id").(int) - repoIDInt64 := int64(repositoryID) repository := &github.Repository{ - ID: &repoIDInt64, + ID: github.Ptr(int64(repoID)), } - _, err = client.Actions.AddSelectedRepoToOrgSecret(ctx, owner, secretName, repository) + _, err := client.Actions.AddSelectedRepoToOrgSecret(ctx, owner, secretName, repository) if err != nil { - return err + return diag.FromErr(err) } - d.SetId(buildTwoPartID(secretName, strconv.Itoa(repositoryID))) - return resourceGithubActionsOrganizationSecretRepositoryRead(d, meta) -} - -func resourceGithubActionsOrganizationSecretRepositoryRead(d *schema.ResourceData, meta any) error { - owner := meta.(*Owner).name - - err := checkOrganization(meta) + id, err := buildID(secretName, strconv.Itoa(repoID)) if err != nil { - return err + return diag.FromErr(err) } - client := meta.(*Owner).v3client + d.SetId(id) - secretName, repositoryIDString, err := parseTwoPartID(d.Id(), "secret_name", "repository_id") - if err != nil { - return err - } + return nil +} - repositoryID, err := strconv.ParseInt(repositoryIDString, 10, 64) - if err != nil { - return unconvertibleIdErr(repositoryIDString, err) +func resourceGithubActionsOrganizationSecretRepositoryRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Get("secret_name").(string) + repoID := int64(d.Get("repository_id").(int)) opt := &github.ListOptions{ PerPage: maxPerPage, } + for { repos, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) if err != nil { - return err + return diag.FromErr(err) } for _, repo := range repos.Repositories { - if repo.GetID() == repositoryID { - if err = d.Set("secret_name", secretName); err != nil { - return err - } - if err = d.Set("repository_id", repositoryID); err != nil { - return err - } + if repo.GetID() == repoID { return nil } } @@ -111,33 +101,52 @@ func resourceGithubActionsOrganizationSecretRepositoryRead(d *schema.ResourceDat opt.Page = resp.NextPage } - log.Printf("[INFO] Removing secret repository association %s from state because it no longer exists in GitHub", - d.Id()) + log.Printf("[INFO] Removing secret repository association %s from state because it no longer exists in GitHub", d.Id()) d.SetId("") return nil } -func resourceGithubActionsOrganizationSecretRepositoryDelete(d *schema.ResourceData, meta any) error { - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubActionsOrganizationSecretRepositoryDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name secretName := d.Get("secret_name").(string) - repositoryID := d.Get("repository_id").(int) + repoID := d.Get("repository_id").(int) - repoIDInt64 := int64(repositoryID) repository := &github.Repository{ - ID: &repoIDInt64, + ID: github.Ptr(int64(repoID)), } - _, err = client.Actions.RemoveSelectedRepoFromOrgSecret(ctx, owner, secretName, repository) + _, err := client.Actions.RemoveSelectedRepoFromOrgSecret(ctx, owner, secretName, repository) if err != nil { - return err + return diag.FromErr(err) } return nil } + +func resourceGithubActionsOrganizationSecretRepositoryImport(ctx context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + secretName, repoIDStr, err := parseID2(d.Id()) + if err != nil { + return nil, err + } + + repoID, err := strconv.Atoi(repoIDStr) + if err != nil { + return nil, err + } + + if err := d.Set("secret_name", secretName); err != nil { + return nil, err + } + if err := d.Set("repository_id", repoID); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_actions_organization_secret_repository_test.go b/github/resource_github_actions_organization_secret_repository_test.go index eabeb13763..ef0ff0e00e 100644 --- a/github/resource_github_actions_organization_secret_repository_test.go +++ b/github/resource_github_actions_organization_secret_repository_test.go @@ -1,6 +1,7 @@ package github import ( + "encoding/base64" "fmt" "testing" @@ -9,35 +10,29 @@ import ( ) func TestAccGithubActionsOrganizationSecretRepository(t *testing.T) { - t.Run("set repository allowlist for a organization secret", func(t *testing.T) { + t.Run("create", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-org-secret-%s", testResourcePrefix, randomID) - secretName := testAccConf.testOrgSecretName - if len(secretName) == 0 { - t.Skip("test organization secret name is not set") - } + secretName := fmt.Sprintf("test_%s", randomID) + secretValue := base64.StdEncoding.EncodeToString([]byte("foo")) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_repository" "test_repo_1" { - name = "%s" - visibility = "internal" - vulnerability_alerts = "true" - } +resource "github_actions_organization_secret" "test" { + secret_name = "%s" + encrypted_value = "%s" + visibility = "selected" +} - resource "github_actions_organization_secret_repository" "org_secret_repo" { - secret_name = "%s" - repository_id = github_repository.test_repo_1.repo_id - } - `, repoName, secretName) +resource "github_repository" "test" { + name = "%s" + visibility = "public" +} - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet( - "github_actions_organization_secret_repository.org_secret_repo", "secret_name", - ), - resource.TestCheckResourceAttr( - "github_actions_organization_secret_repository.org_secret_repo", "repository_id.#", "1", - ), - ) +resource "github_actions_organization_secret_repository" "test" { + secret_name = github_actions_organization_secret.test.secret_name + repository_id = github_repository.test.repo_id +} +`, secretName, secretValue, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -45,7 +40,10 @@ func TestAccGithubActionsOrganizationSecretRepository(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: check, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_actions_organization_secret_repository.test", "secret_name", "github_actions_organization_secret.test", "secret_name"), + resource.TestCheckResourceAttrPair("github_actions_organization_secret_repository.test", "repository_id", "github_repository.test", "repo_id"), + ), }, }, }) diff --git a/github/resource_github_actions_organization_secret_test.go b/github/resource_github_actions_organization_secret_test.go index e846b3e7f8..da469c53ef 100644 --- a/github/resource_github_actions_organization_secret_test.go +++ b/github/resource_github_actions_organization_secret_test.go @@ -319,6 +319,54 @@ resource "github_actions_organization_secret" "test" { }) }) + t.Run("create_update_visibility_selected_no_repo_ids", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlpha) + secretName := fmt.Sprintf("test_%s", randomID) + value := base64.StdEncoding.EncodeToString([]byte("foo")) + valueUpdated := base64.StdEncoding.EncodeToString([]byte("bar")) + + config := ` +resource "github_actions_organization_secret" "test" { + secret_name = "%s" + plaintext_value = "%s" + visibility = "selected" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, secretName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttrSet("github_actions_organization_secret.test", "key_id"), + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "plaintext_value", value), + resource.TestCheckNoResourceAttr("github_actions_organization_secret.test", "encrypted_value"), + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "visibility", "selected"), + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "selected_repository_ids.#", "0"), + resource.TestCheckResourceAttrSet("github_actions_organization_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_organization_secret.test", "updated_at"), + ), + }, + { + Config: fmt.Sprintf(config, secretName, valueUpdated), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttrSet("github_actions_organization_secret.test", "key_id"), + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "plaintext_value", valueUpdated), + resource.TestCheckNoResourceAttr("github_actions_organization_secret.test", "encrypted_value"), + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "visibility", "selected"), + resource.TestCheckResourceAttr("github_actions_organization_secret.test", "selected_repository_ids.#", "0"), + resource.TestCheckResourceAttrSet("github_actions_organization_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_organization_secret.test", "updated_at"), + ), + }, + }, + }) + }) + t.Run("create_update_change_visibility", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlpha) secretName := fmt.Sprintf("test_%s", randomID) diff --git a/github/resource_github_actions_organization_variable.go b/github/resource_github_actions_organization_variable.go index a69be916a1..47d55e3c02 100644 --- a/github/resource_github_actions_organization_variable.go +++ b/github/resource_github_actions_organization_variable.go @@ -142,32 +142,34 @@ func resourceGithubActionsOrganizationVariableRead(ctx context.Context, d *schem return diag.FromErr(err) } - repoIDs := []int64{} if variable.GetVisibility() == "selected" { - opt := &github.ListOptions{ - PerPage: maxPerPage, - } - for { - results, resp, err := client.Actions.ListSelectedReposForOrgVariable(ctx, owner, varName, opt) - if err != nil { - return diag.FromErr(err) + if _, ok := d.GetOk("selected_repository_ids"); ok { + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, } - - for _, repo := range results.Repositories { - repoIDs = append(repoIDs, repo.GetID()) + for { + results, resp, err := client.Actions.ListSelectedReposForOrgVariable(ctx, owner, varName, opt) + if err != nil { + return diag.FromErr(err) + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage } - if resp.NextPage == 0 { - break + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return diag.FromErr(err) } - opt.Page = resp.NextPage } } - if err := d.Set("selected_repository_ids", repoIDs); err != nil { - return diag.FromErr(err) - } - return nil } diff --git a/github/resource_github_actions_organization_variable_repositories.go b/github/resource_github_actions_organization_variable_repositories.go new file mode 100644 index 0000000000..dd524c37e1 --- /dev/null +++ b/github/resource_github_actions_organization_variable_repositories.go @@ -0,0 +1,160 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubActionsOrganizationVariableRepositories() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "variable_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSecretNameFunc, + Description: "Name of the existing variable.", + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Set: schema.HashInt, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Required: true, + Description: "An array of repository ids that can access the organization variable.", + }, + }, + + CreateContext: resourceGithubActionsOrganizationVariableRepositoriesCreateOrUpdate, + ReadContext: resourceGithubActionsOrganizationVariableRepositoriesRead, + UpdateContext: resourceGithubActionsOrganizationVariableRepositoriesCreateOrUpdate, + DeleteContext: resourceGithubActionsOrganizationVariableRepositoriesDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubActionsOrganizationVariableRepositoriesImport, + }, + } +} + +func resourceGithubActionsOrganizationVariableRepositoriesCreateOrUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + variableName := d.Get("variable_name").(string) + repoIDs := []int64{} + + ids := d.Get("selected_repository_ids").(*schema.Set).List() + for _, id := range ids { + repoIDs = append(repoIDs, int64(id.(int))) + } + + _, err := client.Actions.SetSelectedReposForOrgVariable(ctx, owner, variableName, repoIDs) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(variableName) + + return nil +} + +func resourceGithubActionsOrganizationVariableRepositoriesRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + variableName := d.Get("variable_name").(string) + + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, + } + for { + results, resp, err := client.Actions.ListSelectedReposForOrgVariable(ctx, owner, variableName, opt) + if err != nil { + return diag.FromErr(err) + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubActionsOrganizationVariableRepositoriesDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + _, err := client.Actions.SetSelectedReposForOrgVariable(ctx, owner, d.Id(), []int64{}) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubActionsOrganizationVariableRepositoriesImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + variableName := d.Id() + + if err := d.Set("variable_name", variableName); err != nil { + return nil, err + } + + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, + } + for { + results, resp, err := client.Actions.ListSelectedReposForOrgVariable(ctx, owner, variableName, opt) + if err != nil { + return nil, err + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_actions_organization_variable_repositories_test.go b/github/resource_github_actions_organization_variable_repositories_test.go new file mode 100644 index 0000000000..323c2363ce --- /dev/null +++ b/github/resource_github_actions_organization_variable_repositories_test.go @@ -0,0 +1,59 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubActionsOrganizationVariableRepositories(t *testing.T) { + t.Run("create", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + variableName := fmt.Sprintf("test_%s", randomID) + variableValue := "foo" + repoName0 := fmt.Sprintf("%s%s-0", testResourcePrefix, randomID) + repoName1 := fmt.Sprintf("%s%s-1", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_actions_organization_variable" "test" { + variable_name = "%s" + value = "%s" + visibility = "selected" +} + +resource "github_repository" "test_0" { + name = "%s" + visibility = "public" +} + +resource "github_repository" "test_1" { + name = "%s" + visibility = "public" +} + +resource "github_actions_organization_variable_repositories" "test" { + variable_name = github_actions_organization_variable.test.variable_name + selected_repository_ids = [ + github_repository.test_0.repo_id, + github_repository.test_1.repo_id + ] +} +`, variableName, variableValue, repoName0, repoName1) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_actions_organization_variable_repositories.test", "variable_name", "github_actions_organization_variable.test", "variable_name"), + resource.TestCheckResourceAttr("github_actions_organization_variable_repositories.test", "selected_repository_ids.#", "2"), + ), + }, + }, + }) + }) +} diff --git a/github/resource_github_actions_organization_variable_repository.go b/github/resource_github_actions_organization_variable_repository.go new file mode 100644 index 0000000000..431bd734db --- /dev/null +++ b/github/resource_github_actions_organization_variable_repository.go @@ -0,0 +1,152 @@ +package github + +import ( + "context" + "log" + "strconv" + + "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubActionsOrganizationVariableRepository() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "variable_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSecretNameFunc, + Description: "Name of the existing variable.", + }, + "repository_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "The repository ID that can access the organization variable.", + }, + }, + + CreateContext: resourceGithubActionsOrganizationVariableRepositoryCreate, + ReadContext: resourceGithubActionsOrganizationVariableRepositoryRead, + DeleteContext: resourceGithubActionsOrganizationVariableRepositoryDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubActionsOrganizationVariableRepositoryImport, + }, + } +} + +func resourceGithubActionsOrganizationVariableRepositoryCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + variableName := d.Get("variable_name").(string) + repoID := d.Get("repository_id").(int) + + repository := &github.Repository{ + ID: github.Ptr(int64(repoID)), + } + + _, err := client.Actions.AddSelectedRepoToOrgVariable(ctx, owner, variableName, repository) + if err != nil { + return diag.FromErr(err) + } + + id, err := buildID(variableName, strconv.Itoa(repoID)) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + return nil +} + +func resourceGithubActionsOrganizationVariableRepositoryRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + variableName := d.Get("variable_name").(string) + repoID := int64(d.Get("repository_id").(int)) + + opt := &github.ListOptions{ + PerPage: maxPerPage, + } + + for { + repos, resp, err := client.Actions.ListSelectedReposForOrgVariable(ctx, owner, variableName, opt) + if err != nil { + return diag.FromErr(err) + } + + for _, repo := range repos.Repositories { + if repo.GetID() == repoID { + return nil + } + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + log.Printf("[INFO] Removing variable repository association %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + + return nil +} + +func resourceGithubActionsOrganizationVariableRepositoryDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + variableName := d.Get("variable_name").(string) + repoID := d.Get("repository_id").(int) + + repository := &github.Repository{ + ID: github.Ptr(int64(repoID)), + } + _, err := client.Actions.RemoveSelectedRepoFromOrgVariable(ctx, owner, variableName, repository) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubActionsOrganizationVariableRepositoryImport(ctx context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + variableName, repoIDStr, err := parseID2(d.Id()) + if err != nil { + return nil, err + } + + repoID, err := strconv.Atoi(repoIDStr) + if err != nil { + return nil, err + } + + if err := d.Set("variable_name", variableName); err != nil { + return nil, err + } + if err := d.Set("repository_id", repoID); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_actions_organization_variable_repository_test.go b/github/resource_github_actions_organization_variable_repository_test.go new file mode 100644 index 0000000000..30c0ec68dd --- /dev/null +++ b/github/resource_github_actions_organization_variable_repository_test.go @@ -0,0 +1,50 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubActionsOrganizationVariableRepository(t *testing.T) { + t.Run("create", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + variableName := fmt.Sprintf("test_%s", randomID) + variableValue := "foo" + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_actions_organization_variable" "test" { + variable_name = "%s" + value = "%s" + visibility = "selected" +} + +resource "github_repository" "test" { + name = "%s" + visibility = "public" +} + +resource "github_actions_organization_variable_repository" "test" { + variable_name = github_actions_organization_variable.test.variable_name + repository_id = github_repository.test.repo_id +} +`, variableName, variableValue, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_actions_organization_variable_repository.test", "variable_name", "github_actions_organization_variable.test", "variable_name"), + resource.TestCheckResourceAttrPair("github_actions_organization_variable_repository.test", "repository_id", "github_repository.test", "repo_id"), + ), + }, + }, + }) + }) +} diff --git a/github/resource_github_actions_organization_variable_test.go b/github/resource_github_actions_organization_variable_test.go index 9f4100c3c0..cd99944fa4 100644 --- a/github/resource_github_actions_organization_variable_test.go +++ b/github/resource_github_actions_organization_variable_test.go @@ -159,6 +159,50 @@ resource "github_actions_organization_variable" "test" { }) }) + t.Run("create_update_visibility_selected_no_repo_ids", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlpha) + varName := fmt.Sprintf("test_%s", randomID) + value := "foo" + valueUpdated := "bar" + + config := ` +resource "github_actions_organization_variable" "test" { + variable_name = "%s" + value = "%s" + visibility = "selected" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, varName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "variable_name", varName), + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "value", value), + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "visibility", "selected"), + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "selected_repository_ids.#", "0"), + resource.TestCheckResourceAttrSet("github_actions_organization_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_organization_variable.test", "updated_at"), + ), + }, + { + Config: fmt.Sprintf(config, varName, valueUpdated), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "variable_name", varName), + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "value", valueUpdated), + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "visibility", "selected"), + resource.TestCheckResourceAttr("github_actions_organization_variable.test", "selected_repository_ids.#", "0"), + resource.TestCheckResourceAttrSet("github_actions_organization_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_organization_variable.test", "updated_at"), + ), + }, + }, + }) + }) + t.Run("create_update_change_visibility", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlpha) varName := fmt.Sprintf("test_%s", randomID) diff --git a/github/resource_github_dependabot_organization_secret.go b/github/resource_github_dependabot_organization_secret.go index 80ac8b67f4..ed48c986a8 100644 --- a/github/resource_github_dependabot_organization_secret.go +++ b/github/resource_github_dependabot_organization_secret.go @@ -60,6 +60,7 @@ func resourceGithubDependabotOrganizationSecret() *schema.Resource { }, Optional: true, Description: "An array of repository ids that can access the organization secret.", + Deprecated: "This field is deprecated and will be removed in a future release. Please use the `github_dependabot_organization_secret_repositories` or `github_dependabot_organization_secret_repository` resources to manage repository access to organization secrets.", }, "created_at": { Type: schema.TypeString, @@ -211,32 +212,34 @@ func resourceGithubDependabotOrganizationSecretRead(ctx context.Context, d *sche return diag.FromErr(err) } - repoIDs := []int64{} if secret.Visibility == "selected" { - opt := &github.ListOptions{ - PerPage: maxPerPage, - } - for { - results, resp, err := client.Dependabot.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) - if err != nil { - return diag.FromErr(err) + if _, ok := d.GetOk("selected_repository_ids"); ok { + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, } - - for _, repo := range results.Repositories { - repoIDs = append(repoIDs, repo.GetID()) + for { + results, resp, err := client.Dependabot.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) + if err != nil { + return diag.FromErr(err) + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage } - if resp.NextPage == 0 { - break + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return diag.FromErr(err) } - opt.Page = resp.NextPage } } - if err := d.Set("selected_repository_ids", repoIDs); err != nil { - return diag.FromErr(err) - } - return nil } diff --git a/github/resource_github_dependabot_organization_secret_repositories.go b/github/resource_github_dependabot_organization_secret_repositories.go index 39cb3faba0..8c1ab3a3e6 100644 --- a/github/resource_github_dependabot_organization_secret_repositories.go +++ b/github/resource_github_dependabot_organization_secret_repositories.go @@ -4,91 +4,91 @@ import ( "context" "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubDependabotOrganizationSecretRepositories() *schema.Resource { return &schema.Resource{ - Create: resourceGithubDependabotOrganizationSecretRepositoriesCreateOrUpdate, - Read: resourceGithubDependabotOrganizationSecretRepositoriesRead, - Update: resourceGithubDependabotOrganizationSecretRepositoriesCreateOrUpdate, - Delete: resourceGithubDependabotOrganizationSecretRepositoriesDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ "secret_name": { Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Name of the existing secret.", ValidateDiagFunc: validateSecretNameFunc, + Description: "Name of the existing secret.", }, "selected_repository_ids": { Type: schema.TypeSet, + Set: schema.HashInt, Elem: &schema.Schema{ Type: schema.TypeInt, }, - Set: schema.HashInt, Required: true, Description: "An array of repository ids that can access the organization secret.", }, }, + + CreateContext: resourceGithubDependabotOrganizationSecretRepositoriesCreateOrUpdate, + ReadContext: resourceGithubDependabotOrganizationSecretRepositoriesRead, + UpdateContext: resourceGithubDependabotOrganizationSecretRepositoriesCreateOrUpdate, + DeleteContext: resourceGithubDependabotOrganizationSecretRepositoriesDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubDependabotOrganizationSecretRepositoriesImport, + }, } } -func resourceGithubDependabotOrganizationSecretRepositoriesCreateOrUpdate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() - - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubDependabotOrganizationSecretRepositoriesCreateOrUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - secretName := d.Get("secret_name").(string) - selectedRepositories := d.Get("selected_repository_ids") + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - selectedRepositoryIDs := github.DependabotSecretsSelectedRepoIDs{} + secretName := d.Get("secret_name").(string) + repoIDs := []int64{} - ids := selectedRepositories.(*schema.Set).List() + ids := d.Get("selected_repository_ids").(*schema.Set).List() for _, id := range ids { - selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int))) + repoIDs = append(repoIDs, int64(id.(int))) } - _, err = client.Dependabot.SetSelectedReposForOrgSecret(ctx, owner, secretName, selectedRepositoryIDs) + _, err := client.Dependabot.SetSelectedReposForOrgSecret(ctx, owner, secretName, repoIDs) if err != nil { - return err + return diag.FromErr(err) } d.SetId(secretName) - return resourceGithubDependabotOrganizationSecretRepositoriesRead(d, meta) -} -func resourceGithubDependabotOrganizationSecretRepositoriesRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() + return nil +} - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubDependabotOrganizationSecretRepositoriesRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - selectedRepositoryIDs := github.DependabotSecretsSelectedRepoIDs{} + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Get("secret_name").(string) + + repoIDs := []int64{} opt := &github.ListOptions{ PerPage: maxPerPage, } for { - results, resp, err := client.Dependabot.ListSelectedReposForOrgSecret(ctx, owner, d.Id(), opt) + results, resp, err := client.Dependabot.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) if err != nil { - return err + return diag.FromErr(err) } for _, repo := range results.Repositories { - selectedRepositoryIDs = append(selectedRepositoryIDs, repo.GetID()) + repoIDs = append(repoIDs, repo.GetID()) } if resp.NextPage == 0 { @@ -97,28 +97,64 @@ func resourceGithubDependabotOrganizationSecretRepositoriesRead(d *schema.Resour opt.Page = resp.NextPage } - if err = d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { - return err + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return diag.FromErr(err) } return nil } -func resourceGithubDependabotOrganizationSecretRepositoriesDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - - err := checkOrganization(meta) - if err != nil { - return err +func resourceGithubDependabotOrganizationSecretRepositoriesDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) } - selectedRepositoryIDs := github.DependabotSecretsSelectedRepoIDs{} - _, err = client.Dependabot.SetSelectedReposForOrgSecret(ctx, owner, d.Id(), selectedRepositoryIDs) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + _, err := client.Dependabot.SetSelectedReposForOrgSecret(ctx, owner, d.Id(), []int64{}) if err != nil { - return err + return diag.FromErr(err) } return nil } + +func resourceGithubDependabotOrganizationSecretRepositoriesImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Id() + + if err := d.Set("secret_name", secretName); err != nil { + return nil, err + } + + repoIDs := []int64{} + opt := &github.ListOptions{ + PerPage: maxPerPage, + } + for { + results, resp, err := client.Dependabot.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) + if err != nil { + return nil, err + } + + for _, repo := range results.Repositories { + repoIDs = append(repoIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + if err := d.Set("selected_repository_ids", repoIDs); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_dependabot_organization_secret_repositories_test.go b/github/resource_github_dependabot_organization_secret_repositories_test.go index 4ff6354b42..fd66da858a 100644 --- a/github/resource_github_dependabot_organization_secret_repositories_test.go +++ b/github/resource_github_dependabot_organization_secret_repositories_test.go @@ -1,6 +1,7 @@ package github import ( + "encoding/base64" "fmt" "testing" @@ -9,48 +10,48 @@ import ( ) func TestAccGithubDependabotOrganizationSecretRepositories(t *testing.T) { - t.Run("set repository allowlist for an organization secret", func(t *testing.T) { + t.Run("create", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName1 := fmt.Sprintf("%srepo-depbot-org-secret-1-%s", testResourcePrefix, randomID) - repoName2 := fmt.Sprintf("%srepo-depbot-org-secret-2-%s", testResourcePrefix, randomID) + secretName := fmt.Sprintf("test_%s", randomID) + secretValue := base64.StdEncoding.EncodeToString([]byte("foo")) + repoName0 := fmt.Sprintf("%s%s-0", testResourcePrefix, randomID) + repoName1 := fmt.Sprintf("%s%s-1", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_actions_organization_secret" "test" { - secret_name = "TEST" - plaintext_value = "Testing 1..2..3.." - visibility = "all" - } - - resource "github_repository" "test_repo_1" { - name = "%s" - visibility = "private" - vulnerability_alerts = "true" - } - - resource "github_repository" "test_repo_2" { - name = "%s" - visibility = "private" - vulnerability_alerts = "true" - } - - resource "github_dependabot_organization_secret_repositories" "org_secret_repos" { - secret_name = github_actions_organization_secret.test.secret_name - selected_repository_ids = [ - github_repository.test_repo_1.repo_id, - github_repository.test_repo_2.repo_id - ] - } - `, repoName1, repoName2) +resource "github_dependabot_organization_secret" "test" { + secret_name = "%s" + encrypted_value = "%s" + visibility = "selected" +} + +resource "github_repository" "test_0" { + name = "%s" + visibility = "public" +} + +resource "github_repository" "test_1" { + name = "%s" + visibility = "public" +} + +resource "github_dependabot_organization_secret_repositories" "test" { + secret_name = github_dependabot_organization_secret.test.secret_name + selected_repository_ids = [ + github_repository.test_0.repo_id, + github_repository.test_1.repo_id + ] +} +`, secretName, secretValue, repoName0, repoName1) resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("github_dependabot_organization_secret_repositories.org_secret_repos", "secret_name"), - resource.TestCheckResourceAttr("github_dependabot_organization_secret_repositories.org_secret_repos", "selected_repository_ids.#", "2"), + resource.TestCheckResourceAttrPair("github_dependabot_organization_secret_repositories.test", "secret_name", "github_dependabot_organization_secret.test", "secret_name"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret_repositories.test", "selected_repository_ids.#", "2"), ), }, }, diff --git a/github/resource_github_dependabot_organization_secret_repository.go b/github/resource_github_dependabot_organization_secret_repository.go new file mode 100644 index 0000000000..e429a4e7a0 --- /dev/null +++ b/github/resource_github_dependabot_organization_secret_repository.go @@ -0,0 +1,152 @@ +package github + +import ( + "context" + "log" + "strconv" + + "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubDependabotOrganizationSecretRepository() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSecretNameFunc, + Description: "Name of the existing secret.", + }, + "repository_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "The repository ID that can access the organization secret.", + }, + }, + + CreateContext: resourceGithubDependabotOrganizationSecretRepositoryCreate, + ReadContext: resourceGithubDependabotOrganizationSecretRepositoryRead, + DeleteContext: resourceGithubDependabotOrganizationSecretRepositoryDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubDependabotOrganizationSecretRepositoryImport, + }, + } +} + +func resourceGithubDependabotOrganizationSecretRepositoryCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Get("secret_name").(string) + repoID := d.Get("repository_id").(int) + + repository := &github.Repository{ + ID: github.Ptr(int64(repoID)), + } + + _, err := client.Dependabot.AddSelectedRepoToOrgSecret(ctx, owner, secretName, repository) + if err != nil { + return diag.FromErr(err) + } + + id, err := buildID(secretName, strconv.Itoa(repoID)) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + return nil +} + +func resourceGithubDependabotOrganizationSecretRepositoryRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Get("secret_name").(string) + repoID := int64(d.Get("repository_id").(int)) + + opt := &github.ListOptions{ + PerPage: maxPerPage, + } + + for { + repos, resp, err := client.Dependabot.ListSelectedReposForOrgSecret(ctx, owner, secretName, opt) + if err != nil { + return diag.FromErr(err) + } + + for _, repo := range repos.Repositories { + if repo.GetID() == repoID { + return nil + } + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + log.Printf("[INFO] Removing secret repository association %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + + return nil +} + +func resourceGithubDependabotOrganizationSecretRepositoryDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + if err := checkOrganization(m); err != nil { + return diag.FromErr(err) + } + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + secretName := d.Get("secret_name").(string) + repoID := d.Get("repository_id").(int) + + repository := &github.Repository{ + ID: github.Ptr(int64(repoID)), + } + _, err := client.Dependabot.RemoveSelectedRepoFromOrgSecret(ctx, owner, secretName, repository) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubDependabotOrganizationSecretRepositoryImport(ctx context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + secretName, repoIDStr, err := parseID2(d.Id()) + if err != nil { + return nil, err + } + + repoID, err := strconv.Atoi(repoIDStr) + if err != nil { + return nil, err + } + + if err := d.Set("secret_name", secretName); err != nil { + return nil, err + } + if err := d.Set("repository_id", repoID); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_dependabot_organization_secret_repository_test.go b/github/resource_github_dependabot_organization_secret_repository_test.go new file mode 100644 index 0000000000..08280dab4e --- /dev/null +++ b/github/resource_github_dependabot_organization_secret_repository_test.go @@ -0,0 +1,51 @@ +package github + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubDependabotOrganizationSecretRepository(t *testing.T) { + t.Run("create", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + secretName := fmt.Sprintf("test_%s", randomID) + secretValue := base64.StdEncoding.EncodeToString([]byte("foo")) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_dependabot_organization_secret" "test" { + secret_name = "%s" + encrypted_value = "%s" + visibility = "selected" +} + +resource "github_repository" "test" { + name = "%s" + visibility = "public" +} + +resource "github_dependabot_organization_secret_repository" "test" { + secret_name = github_dependabot_organization_secret.test.secret_name + repository_id = github_repository.test.repo_id +} +`, secretName, secretValue, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_dependabot_organization_secret_repository.test", "secret_name", "github_dependabot_organization_secret.test", "secret_name"), + resource.TestCheckResourceAttrPair("github_dependabot_organization_secret_repository.test", "repository_id", "github_repository.test", "repo_id"), + ), + }, + }, + }) + }) +} diff --git a/github/resource_github_dependabot_organization_secret_test.go b/github/resource_github_dependabot_organization_secret_test.go index 66058b5032..ef55e98d22 100644 --- a/github/resource_github_dependabot_organization_secret_test.go +++ b/github/resource_github_dependabot_organization_secret_test.go @@ -319,6 +319,54 @@ resource "github_dependabot_organization_secret" "test" { }) }) + t.Run("create_update_visibility_selected_no_repo_ids", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlpha) + secretName := fmt.Sprintf("test_%s", randomID) + value := base64.StdEncoding.EncodeToString([]byte("foo")) + valueUpdated := base64.StdEncoding.EncodeToString([]byte("bar")) + + config := ` +resource "github_dependabot_organization_secret" "test" { + secret_name = "%s" + plaintext_value = "%s" + visibility = "selected" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, secretName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttrSet("github_dependabot_organization_secret.test", "key_id"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "plaintext_value", value), + resource.TestCheckNoResourceAttr("github_dependabot_organization_secret.test", "encrypted_value"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "visibility", "selected"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "selected_repository_ids.#", "0"), + resource.TestCheckResourceAttrSet("github_dependabot_organization_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_dependabot_organization_secret.test", "updated_at"), + ), + }, + { + Config: fmt.Sprintf(config, secretName, valueUpdated), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttrSet("github_dependabot_organization_secret.test", "key_id"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "plaintext_value", valueUpdated), + resource.TestCheckNoResourceAttr("github_dependabot_organization_secret.test", "encrypted_value"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "visibility", "selected"), + resource.TestCheckResourceAttr("github_dependabot_organization_secret.test", "selected_repository_ids.#", "0"), + resource.TestCheckResourceAttrSet("github_dependabot_organization_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_dependabot_organization_secret.test", "updated_at"), + ), + }, + }, + }) + }) + t.Run("create_update_change_visibility", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlpha) secretName := fmt.Sprintf("test_%s", randomID) diff --git a/github/resource_github_emu_group_mapping.go b/github/resource_github_emu_group_mapping.go index 17235683c8..38d45a5de4 100644 --- a/github/resource_github_emu_group_mapping.go +++ b/github/resource_github_emu_group_mapping.go @@ -2,7 +2,7 @@ package github import ( "context" - "fmt" + "net/http" "strconv" "github.com/google/go-github/v82/github" @@ -13,14 +13,21 @@ import ( func resourceGithubEMUGroupMapping() *schema.Resource { return &schema.Resource{ - CreateContext: resourceGithubEMUGroupMappingCreateOrUpdate, + CreateContext: resourceGithubEMUGroupMappingCreate, ReadContext: resourceGithubEMUGroupMappingRead, - UpdateContext: resourceGithubEMUGroupMappingCreateOrUpdate, + UpdateContext: resourceGithubEMUGroupMappingUpdate, DeleteContext: resourceGithubEMUGroupMappingDelete, Importer: &schema.ResourceImporter{ StateContext: resourceGithubEMUGroupMappingImport, }, + CustomizeDiff: diffTeam, + Description: "Manages the mapping of an external group to a GitHub team.", Schema: map[string]*schema.Schema{ + "team_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the GitHub team.", + }, "team_slug": { Type: schema.TypeString, Required: true, @@ -29,20 +36,32 @@ func resourceGithubEMUGroupMapping() *schema.Resource { "group_id": { Type: schema.TypeInt, Required: true, + ForceNew: true, Description: "Integer corresponding to the external group ID to be linked.", }, + "group_name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the external group.", + }, "etag": { Type: schema.TypeString, Computed: true, }, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubEMUGroupMappingV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubEMUGroupMappingStateUpgradeV0, + Version: 0, + }, + }, } } -func resourceGithubEMUGroupMappingRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - tflog.Trace(ctx, "Reading EMU group mapping", map[string]any{ - "resource_id": d.Id(), - }) +func resourceGithubEMUGroupMappingCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + tflog.Trace(ctx, "Creating EMU group mapping") err := checkOrganization(meta) if err != nil { @@ -50,77 +69,67 @@ func resourceGithubEMUGroupMappingRead(ctx context.Context, d *schema.ResourceDa } client := meta.(*Owner).v3client orgName := meta.(*Owner).name + tflog.SetField(ctx, "org_name", orgName) - id, ok := d.GetOk("group_id") - if !ok { - return diag.Errorf("could not get group id from provided value") + teamSlug := d.Get("team_slug").(string) + tflog.SetField(ctx, "team_slug", teamSlug) + + groupID := toInt64(d.Get("group_id")) + tflog.SetField(ctx, "group_id", groupID) + eg := &github.ExternalGroup{ + GroupID: github.Ptr(groupID), } - id64, err := getInt64FromInterface(id) + + tflog.Debug(ctx, "Connecting external group to team via GitHub API") + + group, resp, err := client.Teams.UpdateConnectedExternalGroup(ctx, orgName, teamSlug, eg) if err != nil { return diag.FromErr(err) } - tflog.Debug(ctx, "Querying external group from GitHub API", map[string]any{ - "org_name": orgName, - "group_id": id64, - }) + tflog.Debug(ctx, "Successfully updated connected external group") - group, resp, err := client.Teams.GetExternalGroup(ctx, orgName, id64) + teamID, err := lookupTeamID(ctx, meta.(*Owner), teamSlug) if err != nil { - if resp != nil && resp.StatusCode == 404 { - // If the group is not found, remove it from state - tflog.Info(ctx, "Removing EMU group mapping from state because it no longer exists in GitHub", map[string]any{ - "org_name": orgName, - "group_id": id64, - "resource_id": d.Id(), - "status_code": resp.StatusCode, - }) - d.SetId("") - return nil - } return diag.FromErr(err) } - tflog.Debug(ctx, "Successfully retrieved external group from GitHub API", map[string]any{ - "org_name": orgName, - "group_id": id64, - "team_count": len(group.Teams), - }) + newResourceID, err := buildID(strconv.FormatInt(teamID, 10), teamSlug, strconv.FormatInt(groupID, 10)) + if err != nil { + return diag.FromErr(err) + } - if len(group.Teams) < 1 { - // if there's not a team linked, that means it was removed outside of terraform - // and we should remove it from our state - tflog.Info(ctx, "Removing EMU group mapping from state because no teams are linked", map[string]any{ - "org_name": orgName, - "group_id": id64, - "resource_id": d.Id(), - }) - d.SetId("") - return nil + if err := d.Set("team_id", teamID); err != nil { + return diag.FromErr(err) } + tflog.Trace(ctx, "Setting resource ID", map[string]any{ + "resource_id": newResourceID, + }) + d.SetId(newResourceID) + etag := resp.Header.Get("ETag") tflog.Trace(ctx, "Setting state attribute: etag", map[string]any{ "etag": etag, }) - if err = d.Set("etag", etag); err != nil { + if err := d.Set("etag", etag); err != nil { return diag.FromErr(err) } - groupIDInt := int(group.GetGroupID()) - tflog.Trace(ctx, "Setting state attribute: group_id", map[string]any{ - "group_id": groupIDInt, - }) - if err = d.Set("group_id", groupIDInt); err != nil { + if err := d.Set("group_name", group.GetGroupName()); err != nil { return diag.FromErr(err) } + + tflog.Trace(ctx, "Resource created or updated successfully", map[string]any{ + "resource_id": d.Id(), + }) + return nil } -func resourceGithubEMUGroupMappingCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - resourceID := d.Id() - tflog.Trace(ctx, "Creating or updating EMU group mapping", map[string]any{ - "resource_id": resourceID, +func resourceGithubEMUGroupMappingRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + tflog.Trace(ctx, "Reading EMU group mapping", map[string]any{ + "resource_id": d.Id(), }) err := checkOrganization(meta) @@ -130,60 +139,133 @@ func resourceGithubEMUGroupMappingCreateOrUpdate(ctx context.Context, d *schema. client := meta.(*Owner).v3client orgName := meta.(*Owner).name - teamSlug, ok := d.GetOk("team_slug") - if !ok { - return diag.Errorf("could not get team slug from provided value") + groupID := toInt64(d.Get("group_id")) + teamSlug := d.Get("team_slug").(string) + + tflog.SetField(ctx, "group_id", groupID) + tflog.SetField(ctx, "team_slug", teamSlug) + tflog.SetField(ctx, "org_name", orgName) + + tflog.Debug(ctx, "Querying external groups linked to team from GitHub API") + + groupsList, resp, err := client.Teams.ListExternalGroupsForTeamBySlug(ctx, orgName, teamSlug) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusBadRequest { + tflog.Info(ctx, "Removing EMU group mapping from state because the team has explicit members in GitHub", map[string]any{ + "resource_id": d.Id(), + }) + d.SetId("") + return nil + } + if resp != nil && (resp.StatusCode == http.StatusNotFound) { + // If the Group is not found, remove it from state + tflog.Info(ctx, "Removing EMU group mapping from state because team no longer exists in GitHub", map[string]any{ + "resource_id": d.Id(), + }) + d.SetId("") + return nil + } + return diag.FromErr(err) } - id, ok := d.GetOk("group_id") - if !ok { - return diag.Errorf("could not get group id from provided value") + if len(groupsList.Groups) < 1 { + tflog.Info(ctx, "Removing EMU group mapping from state because no external groups are linked to the team", map[string]any{ + "resource_id": d.Id(), + }) + d.SetId("") + return nil } - id64, err := getInt64FromInterface(id) - if err != nil { + + // A team can only be linked to one external group + group := groupsList.Groups[0] + + tflog.Debug(ctx, "Successfully retrieved external group from GitHub API", map[string]any{ + "group_id": group.GetGroupID(), + "group_name": group.GetGroupName(), + }) + + if group.GetGroupID() != groupID { + return diag.Errorf("group id mismatch: %d != %d", group.GetGroupID(), groupID) + } + + etag := resp.Header.Get("ETag") + if err := d.Set("etag", etag); err != nil { return diag.FromErr(err) } - teamSlugStr := teamSlug.(string) + if err := d.Set("group_id", int(group.GetGroupID())); err != nil { + return diag.FromErr(err) + } - eg := &github.ExternalGroup{ - GroupID: &id64, + if err := d.Set("group_name", group.GetGroupName()); err != nil { + return diag.FromErr(err) } - tflog.Debug(ctx, "Updating connected external group via GitHub API", map[string]any{ - "org_name": orgName, - "team_slug": teamSlugStr, - "group_id": id64, + return nil +} + +func resourceGithubEMUGroupMappingUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + tflog.Trace(ctx, "Updating EMU group mapping", map[string]any{ + "resource_id": d.Id(), }) - _, resp, err := client.Teams.UpdateConnectedExternalGroup(ctx, orgName, teamSlugStr, eg) + err := checkOrganization(meta) if err != nil { return diag.FromErr(err) } + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + tflog.SetField(ctx, "org_name", orgName) - tflog.Debug(ctx, "Successfully updated connected external group", map[string]any{ - "org_name": orgName, - "team_slug": teamSlugStr, - "group_id": id64, - }) + teamSlug := d.Get("team_slug").(string) + tflog.SetField(ctx, "team_slug", teamSlug) - newResourceID := fmt.Sprintf("teams/%s/external-groups", teamSlugStr) - tflog.Trace(ctx, "Setting resource ID", map[string]any{ - "resource_id": newResourceID, - }) - d.SetId(newResourceID) + groupID := toInt64(d.Get("group_id")) + tflog.SetField(ctx, "group_id", groupID) + eg := &github.ExternalGroup{ + GroupID: github.Ptr(groupID), + } - etag := resp.Header.Get("ETag") - tflog.Trace(ctx, "Setting state attribute: etag", map[string]any{ - "etag": etag, - }) - if err = d.Set("etag", etag); err != nil { - return diag.FromErr(err) + if d.HasChanges("group_id", "team_slug") { + + tflog.Debug(ctx, "Updating connected external group via GitHub API") + + group, resp, err := client.Teams.UpdateConnectedExternalGroup(ctx, orgName, teamSlug, eg) + if err != nil { + return diag.FromErr(err) + } + + tflog.Debug(ctx, "Successfully updated connected external group") + + etag := resp.Header.Get("ETag") + tflog.Trace(ctx, "Setting state attribute: etag", map[string]any{ + "etag": etag, + }) + if err := d.Set("etag", etag); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("group_name", group.GetGroupName()); err != nil { + return diag.FromErr(err) + } + + teamID := toInt64(d.Get("team_id")) + + newResourceID, err := buildID(strconv.FormatInt(teamID, 10), teamSlug, strconv.FormatInt(groupID, 10)) + if err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, "Setting resource ID", map[string]any{ + "resource_id": newResourceID, + }) + d.SetId(newResourceID) } - tflog.Trace(ctx, "Resource created or updated successfully", map[string]any{ - "resource_id": newResourceID, + tflog.Trace(ctx, "Updated successfully", map[string]any{ + "resource_id": d.Id(), }) + return nil } @@ -224,25 +306,6 @@ func resourceGithubEMUGroupMappingDelete(ctx context.Context, d *schema.Resource return nil } -func getInt64FromInterface(val any) (int64, error) { - var id64 int64 - switch val := val.(type) { - case int64: - id64 = val - case int: - id64 = int64(val) - case string: - var err error - id64, err = strconv.ParseInt(val, 10, 64) - if err != nil { - return 0, fmt.Errorf("could not parse id from string: %w", err) - } - default: - return 0, fmt.Errorf("unexpected type converting to int64 from interface") - } - return id64, nil -} - func resourceGithubEMUGroupMappingImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { importID := d.Id() tflog.Trace(ctx, "Importing EMU group mapping with two-part ID", map[string]any{ @@ -250,7 +313,8 @@ func resourceGithubEMUGroupMappingImport(ctx context.Context, d *schema.Resource "strategy": "two_part_id", }) - groupIDString, teamSlug, err := parseTwoPartID(d.Id(), "group_id", "team_slug") + // : + groupIDString, teamSlug, err := parseID2(d.Id()) if err != nil { return nil, err } @@ -265,6 +329,15 @@ func resourceGithubEMUGroupMappingImport(ctx context.Context, d *schema.Resource "team_slug": teamSlug, }) + teamID, err := lookupTeamID(ctx, meta.(*Owner), teamSlug) + if err != nil { + return nil, err + } + + if err := d.Set("team_id", teamID); err != nil { + return nil, err + } + if err := d.Set("group_id", groupID); err != nil { return nil, err } @@ -273,10 +346,15 @@ func resourceGithubEMUGroupMappingImport(ctx context.Context, d *schema.Resource return nil, err } - resourceID := fmt.Sprintf("teams/%s/external-groups", teamSlug) + resourceID, err := buildID(strconv.FormatInt(teamID, 10), teamSlug, groupIDString) + if err != nil { + return nil, err + } + tflog.Trace(ctx, "Setting resource ID", map[string]any{ "resource_id": resourceID, }) d.SetId(resourceID) + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_emu_group_mapping_migration.go b/github/resource_github_emu_group_mapping_migration.go new file mode 100644 index 0000000000..691ad1a733 --- /dev/null +++ b/github/resource_github_emu_group_mapping_migration.go @@ -0,0 +1,68 @@ +package github + +import ( + "context" + "net/http" + "strconv" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubEMUGroupMappingV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "team_slug": { + Type: schema.TypeString, + Required: true, + Description: "Slug of the GitHub team.", + }, + "group_id": { + Type: schema.TypeInt, + Required: true, + Description: "Integer corresponding to the external group ID to be linked.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceGithubEMUGroupMappingStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + orgName := meta.(*Owner).name + tflog.Trace(ctx, "GitHub EMU Group Mapping State before migration", map[string]any{"state": rawState, "owner": orgName}) + + client := meta.(*Owner).v3client + + teamSlug := rawState["team_slug"].(string) + // We need to bypass the etag because we need to get the latest group + ctx = context.WithValue(ctx, ctxEtag, nil) + groupsList, resp, err := client.Teams.ListExternalGroupsForTeamBySlug(ctx, orgName, teamSlug) + if err != nil { + if resp != nil && (resp.StatusCode == http.StatusNotFound) { + // If the Group is not found, remove it from state + tflog.Info(ctx, "Removing EMU group mapping from state because team no longer exists in GitHub", map[string]any{ + "resource_id": rawState["id"], + }) + return nil, err + } + return nil, err + } + + group := groupsList.Groups[0] + teamID, err := lookupTeamID(ctx, meta.(*Owner), teamSlug) + if err != nil { + return nil, err + } + rawState["team_id"] = teamID + resourceID, err := buildID(strconv.FormatInt(teamID, 10), teamSlug, strconv.FormatInt(group.GetGroupID(), 10)) + if err != nil { + return nil, err + } + rawState["id"] = resourceID + + tflog.Trace(ctx, "GitHub EMU Group Mapping State after migration", map[string]any{"state": rawState}) + return rawState, nil +} diff --git a/github/resource_github_emu_group_mapping_migration_test.go b/github/resource_github_emu_group_mapping_migration_test.go new file mode 100644 index 0000000000..2c44de49c2 --- /dev/null +++ b/github/resource_github_emu_group_mapping_migration_test.go @@ -0,0 +1,134 @@ +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v82/github" +) + +func buildMockResponsesForMigrationV0toV1(mockResponsesOptions mockResponsesOptionsEMUGroupMappingMigrationV0V1) []*mockResponse { + responseBodyJson, err := json.Marshal(mockResponsesOptions.ExternalGroupList) + if err != nil { + panic(fmt.Sprintf("error marshalling external groups response: %s", err)) + } + + mockTeamResponseJson, err := json.Marshal(mockResponsesOptions.Team) + if err != nil { + panic(fmt.Sprintf("error marshalling mock team response: %s", err)) + } + return []*mockResponse{ + { + ExpectedUri: fmt.Sprintf("/orgs/%s/teams/%s/external-groups", mockResponsesOptions.OrgSlug, mockResponsesOptions.TeamSlug), + ExpectedHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + }, + ResponseBody: string(responseBodyJson), + StatusCode: mockResponsesOptions.externalGroupsResponseStatusCode, + }, + { + ExpectedUri: fmt.Sprintf("/orgs/%s/teams/%s", mockResponsesOptions.OrgSlug, mockResponsesOptions.TeamSlug), + ExpectedHeaders: map[string]string{ + "Accept": "application/vnd.github.v3+json", + }, + ResponseBody: string(mockTeamResponseJson), + StatusCode: mockResponsesOptions.teamResponseStatusCode, + }, + } +} + +type mockResponsesOptionsEMUGroupMappingMigrationV0V1 struct { + OrgSlug string + TeamSlug string + externalGroupsResponseStatusCode int + teamResponseStatusCode int + ExternalGroupList github.ExternalGroupList + Team github.Team +} + +func Test_resourceGithubEMUGroupMappingStateUpgradeV0(t *testing.T) { + t.Parallel() + + const testOrgSlug = "test-org" + const testTeamSlug = "test-team" + const testTeamID = 432574718 + const testGroupID = 1234567890 + + meta := &Owner{ + name: testOrgSlug, + } + + for _, d := range []struct { + testName string + rawState map[string]any + want map[string]any + shouldError bool + mockResponsesOptions mockResponsesOptionsEMUGroupMappingMigrationV0V1 + }{ + { + testName: "migrates v0 to v1", + rawState: map[string]any{ + "id": fmt.Sprintf("teams/%s/%d/external-groups", testTeamSlug, testGroupID), + "team_slug": testTeamSlug, + "group_id": testGroupID, + }, + want: map[string]any{ + "id": fmt.Sprintf("%d:%s:%d", testTeamID, testTeamSlug, testGroupID), + "team_slug": testTeamSlug, + "team_id": int64(testTeamID), + "group_id": testGroupID, + }, + shouldError: false, + mockResponsesOptions: mockResponsesOptionsEMUGroupMappingMigrationV0V1{ + OrgSlug: testOrgSlug, + TeamSlug: testTeamSlug, + externalGroupsResponseStatusCode: 201, + teamResponseStatusCode: 200, + ExternalGroupList: github.ExternalGroupList{ + Groups: []*github.ExternalGroup{{ + GroupID: github.Ptr(int64(testGroupID)), + GroupName: github.Ptr(testOrgSlug), + UpdatedAt: github.Ptr(github.Timestamp{Time: time.Now()}), + }}, + }, + Team: github.Team{ + ID: github.Ptr(int64(testTeamID)), + }, + }, + }, + } { + t.Run(d.testName, func(t *testing.T) { + t.Parallel() + + ts := githubApiMock(buildMockResponsesForMigrationV0toV1(d.mockResponsesOptions)) + defer ts.Close() + + httpCl := http.DefaultClient + httpCl.Transport = http.DefaultTransport + + client := github.NewClient(httpCl) + u, _ := url.Parse(ts.URL + "/") + client.BaseURL = u + meta.v3client = client + + currentState := d.rawState + got, err := resourceGithubEMUGroupMappingStateUpgradeV0(t.Context(), currentState, meta) + expectedState := d.want + didError := err != nil + if d.shouldError && !didError { + t.Fatalf("state upgrade should have returned an error. Instead got: %#v", got) + } + if !d.shouldError && didError { + t.Fatalf("state upgrade should not have returned an error. Instead got: %s", err.Error()) + } + if diff := cmp.Diff(expectedState, got); diff != "" { + t.Fatalf("state upgrade returned unexpected state. Diff: %s", diff) + } + }) + } +} diff --git a/github/resource_github_emu_group_mapping_test.go b/github/resource_github_emu_group_mapping_test.go index 678a0c95ce..209572a27e 100644 --- a/github/resource_github_emu_group_mapping_test.go +++ b/github/resource_github_emu_group_mapping_test.go @@ -126,6 +126,51 @@ func TestAccGithubEMUGroupMapping(t *testing.T) { }, }) }) + + t.Run("forces new when switching to different team", func(t *testing.T) { + t.Skip("Skipping this test because we don't have terraform-plugin-testing available yet.") + randomID := acctest.RandString(5) + teamName1 := fmt.Sprintf("%semu1-%s", testResourcePrefix, randomID) + teamName2 := fmt.Sprintf("%semu2-%s", testResourcePrefix, randomID) + + config := ` +resource "github_team" "test1" { + name = "%s" + description = "EMU group mapping test team 1" +} +resource "github_team" "test2" { + name = "%s" + description = "EMU group mapping test team 2" +} +resource "github_emu_group_mapping" "test" { + team_slug = github_team.%s.slug + group_id = %d +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGithubEMUGroupMappingDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, teamName1, teamName2, "test1", groupID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_emu_group_mapping.test", "team_slug", teamName1), + ), + }, + { + Config: fmt.Sprintf(config, teamName1, teamName2, "test2", groupID), + // ConfigPlanChecks: resource.ConfigPlanChecks{ + // PreApply: []plancheck.PlanCheck{ + // plancheckExpectKnownValues("github_emu_group_mapping.test", "team_slug", teamName2), + // plancheck.ExpectResourceAction("github_emu_group_mapping.test", plancheck.ResourceActionDestroyBeforeCreate), // Verify that ForceNew is triggered + // }, + // }, + }, + }, + }) + }) } func testAccCheckGithubEMUGroupMappingDestroy(s *terraform.State) error { diff --git a/github/util.go b/github/util.go index a8a2d96f71..e277299715 100644 --- a/github/util.go +++ b/github/util.go @@ -342,3 +342,43 @@ func deleteResourceOn404AndSwallow304OtherwiseReturnError(err error, d *schema.R } return err } + +// Helper function to safely convert interface{} to int, handling both int and float64. +func toInt(v any) int { + switch val := v.(type) { + case int: + return val + case float64: + return int(val) + case int64: + return int(val) + default: + return 0 + } +} + +// Helper function to safely convert interface{} to int64, handling both int and float64. +func toInt64(v any) int64 { + switch val := v.(type) { + case int: + return int64(val) + case int64: + return val + case float64: + return int64(val) + default: + return 0 + } +} + +// lookupTeamID looks up the ID of a team by its slug. +func lookupTeamID(ctx context.Context, meta *Owner, slug string) (int64, error) { + client := meta.v3client + owner := meta.name + + team, _, err := client.Teams.GetTeamBySlug(ctx, owner, slug) + if err != nil { + return 0, err + } + return team.GetID(), nil +} diff --git a/github/util_diff.go b/github/util_diff.go index 42f20d9e61..9b036d2f00 100644 --- a/github/util_diff.go +++ b/github/util_diff.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -109,3 +110,53 @@ func diffSecretVariableVisibility(ctx context.Context, d *schema.ResourceDiff, _ return nil } + +// diffTeam compares the team_id and team_slug fields to determine if the team has changed. +func diffTeam(ctx context.Context, diff *schema.ResourceDiff, m any) error { + // Skip for new resources - no existing team_id to compare against + if len(diff.Id()) == 0 { + return nil + } + + if diff.HasChange("team_slug") { + if isNewTeamID(ctx, diff, m) { + return diff.ForceNew("team_slug") + } + } + + return nil +} + +// helper function to determine if the team has changed or was renamed. +func isNewTeamID(ctx context.Context, diff *schema.ResourceDiff, m any) bool { + // Get old team_id from state + oldTeamID := toInt64(diff.Get("team_id")) + if oldTeamID == 0 { + return false + } + meta := m.(*Owner) + + // Resolve new team_slug to team ID via API + oldTeamSlug, newTeamSlug := diff.GetChange("team_slug") + newTeamID, err := lookupTeamID(ctx, meta, newTeamSlug.(string)) + if err != nil { + // If team doesn't exist or API fails, skip ForceNew check and let Read handle it + tflog.Debug(ctx, "Unable to resolve new team_slug to team ID, skipping ForceNew check", map[string]any{ + "new_team_slug": newTeamSlug, + "error": err.Error(), + }) + return false + } + + if newTeamID != oldTeamID { + tflog.Debug(ctx, "Team ID changed, forcing new resource", map[string]any{ + "old_team_id": oldTeamID, + "new_team_id": newTeamID, + "new_team_slug": newTeamSlug, + "old_team_slug": oldTeamSlug, + }) + return true + } + + return false +} diff --git a/github/util_rules.go b/github/util_rules.go index ebf84e679f..0350daf11a 100644 --- a/github/util_rules.go +++ b/github/util_rules.go @@ -10,34 +10,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -// Helper function to safely convert interface{} to int, handling both int and float64. -func toInt(v any) int { - switch val := v.(type) { - case int: - return val - case float64: - return int(val) - case int64: - return int(val) - default: - return 0 - } -} - -// Helper function to safely convert interface{} to int64, handling both int and float64. -func toInt64(v any) int64 { - switch val := v.(type) { - case int: - return int64(val) - case int64: - return val - case float64: - return int64(val) - default: - return 0 - } -} - func toPullRequestMergeMethods(input any) []github.PullRequestMergeMethod { value, ok := input.([]any) if !ok || len(value) == 0 { diff --git a/go.mod b/go.mod index 7f0f22d0b8..f1ba41382d 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/integrations/terraform-provider-github/v6 -go 1.24.0 +go 1.24.4 require ( github.com/go-jose/go-jose/v3 v3.0.4 + github.com/google/go-cmp v0.7.0 github.com/google/go-github/v82 v82.0.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.5.0 @@ -21,7 +22,6 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect diff --git a/website/docs/r/actions_organization_secret_repositories.html.markdown b/website/docs/r/actions_organization_secret_repositories.html.markdown index b2d47e3dc3..6e822b19c2 100644 --- a/website/docs/r/actions_organization_secret_repositories.html.markdown +++ b/website/docs/r/actions_organization_secret_repositories.html.markdown @@ -2,12 +2,12 @@ layout: "github" page_title: "GitHub: github_actions_organization_secret_repositories" description: |- - Manages repository allow list for an Action Secret within a GitHub organization + Manages repository allow list for an Actions Secret within a GitHub organization. --- # github_actions_organization_secret_repositories -This resource allows you to manage repository allow list for existing GitHub Actions secrets within your GitHub organization. +This resource allows you to manage the repositories allowed to access an actions secret within your GitHub organization. You must have write access to an organization secret to use this resource. This resource is only applicable when `visibility` of the existing organization secret has been set to `selected`. @@ -15,13 +15,20 @@ This resource is only applicable when `visibility` of the existing organization ## Example Usage ```hcl -data "github_repository" "repo" { - full_name = "my-org/repo" +resource "github_actions_organization_secret" "example" { + secret_name = "mysecret" + plaintext_value = "foo" + visibility = "selected" } -resource "github_actions_organization_secret_repositories" "org_secret_repos" { - secret_name = "existing_secret_name" - selected_repository_ids = [data.github_repository.repo.repo_id] +resource "github_repository" "example" { + name = "myrepo" + visibility = "public" +} + +resource "github_actions_organization_secret_repositories" "example" { + secret_name = github_actions_organization_secret.example.name + selected_repository_ids = [ github_repository.example.repo_id ] } ``` @@ -29,13 +36,28 @@ resource "github_actions_organization_secret_repositories" "org_secret_repos" { The following arguments are supported: -* `secret_name` - (Required) Name of the existing secret -* `selected_repository_ids` - (Required) An array of repository ids that can access the organization secret. +- `secret_name` - (Required) Name of the actions organization secret. +- `selected_repository_ids` - (Required) List of IDs for the repositories that should be able to access the secret. ## Import -This resource can be imported using an ID made up of the secret name: +This resource can be imported using the secret name as the ID. + +### Import Block +The following import block imports the repositories able to access the actions organization secret named `mysecret` to a `github_actions_organization_secret_repositories` resource named `example`. + +```hcl +import { + to = github_actions_organization_secret_repositories.example + id = "mysecret" +} ``` -$ terraform import github_actions_organization_secret_repositories.test_secret_repos test_secret_name + +### Import Command + +The following command imports the repositories able to access the actions organization secret named `mysecret` to a `github_actions_organization_secret_repositories` resource named `example`. + +```shell +terraform import github_actions_organization_secret_repositories.example mysecret ``` diff --git a/website/docs/r/actions_organization_secret_repository.html.markdown b/website/docs/r/actions_organization_secret_repository.html.markdown index 0fd9f54711..4876cf0363 100644 --- a/website/docs/r/actions_organization_secret_repository.html.markdown +++ b/website/docs/r/actions_organization_secret_repository.html.markdown @@ -2,12 +2,12 @@ layout: "github" page_title: "GitHub: github_actions_organization_secret_repository" description: |- - Adds/remove a repository to an organization secret when the visibility for repository access is set to selected. + Add access for a repository to an Actions Secret within a GitHub organization. --- # github_actions_organization_secret_repository -This resource help you to allow/unallow a repository to use an existing GitHub Actions secrets within your GitHub organization. +This resource adds permission for a repository to use an actions secret within your GitHub organization. You must have write access to an organization secret to use this resource. This resource is only applicable when `visibility` of the existing organization secret has been set to `selected`. @@ -15,13 +15,20 @@ This resource is only applicable when `visibility` of the existing organization ## Example Usage ```hcl -data "github_repository" "repo" { - full_name = "my-org/repo" +resource "github_actions_organization_secret" "example" { + secret_name = "mysecret" + plaintext_value = "foo" + visibility = "selected" } -resource "github_actions_organization_secret_repository" "org_secret_repos" { - secret_name = "EXAMPLE_SECRET_NAME" - repository_id = github_repository.repo.repo_id +resource "github_repository" "example" { + name = "myrepo" + visibility = "public" +} + +resource "github_actions_organization_secret_repository" "example" { + secret_name = github_actions_organization_secret.example.name + repository_id = github_repository.example.repo_id } ``` @@ -29,13 +36,28 @@ resource "github_actions_organization_secret_repository" "org_secret_repos" { The following arguments are supported: -* `secret_name` - (Required) Name of the existing secret -* `repository_id` - (Required) Repository id that can access the organization secret. +- `secret_name` - (Required) Name of the actions organization secret. +- `repository_id` - (Required) ID of the repository that should be able to access the secret. ## Import -This resource can be imported using an ID made up of the secret name: +This resource can be imported using an ID made of the secret name and repository name separated by a `:`. + +### Import Block +The following import block imports the access of repository ID `123456` for the actions organization secret named `mysecret` to a `github_actions_organization_secret_repository` resource named `example`. + +```hcl +import { + to = github_actions_organization_secret_repository.example + id = "mysecret:123456" +} ``` -$ terraform import github_actions_organization_secret_repository.test_secret_repos test_secret_name:repo_id + +### Import Command + +The following command imports the access of repository ID `123456` for the actions organization secret named `mysecret` to a `github_actions_organization_secret_repository` resource named `example`. + +```shell +terraform import github_actions_organization_secret_repository.example mysecret:123456 ``` diff --git a/website/docs/r/actions_organization_variable_repositories.html.markdown b/website/docs/r/actions_organization_variable_repositories.html.markdown new file mode 100644 index 0000000000..56975c302f --- /dev/null +++ b/website/docs/r/actions_organization_variable_repositories.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "github" +page_title: "GitHub: github_actions_organization_variable_repositories" +description: |- + Manages repository allow list for an Actions Variable within a GitHub organization. +--- + +# github_actions_organization_variable_repositories + +This resource allows you to manage the repositories allowed to access an actions variable within your GitHub organization. +You must have write access to an organization variable to use this resource. + +This resource is only applicable when `visibility` of the existing organization variable has been set to `selected`. + +## Example Usage + +```hcl +resource "github_actions_organization_variable" "example" { + variable_name = "myvariable" + plaintext_value = "foo" + visibility = "selected" +} + +resource "github_repository" "example" { + name = "myrepo" + visibility = "public" +} + +resource "github_actions_organization_variable_repositories" "example" { + variable_name = github_actions_organization_variable.example.name + selected_repository_ids = [ github_repository.example.repo_id ] +} +``` + +## Argument Reference + +The following arguments are supported: + +- `variable_name` - (Required) Name of the actions organization variable. +- `selected_repository_ids` - (Required) List of IDs for the repositories that should be able to access the variable. + +## Import + +This resource can be imported using the variable name as the ID. + +### Import Block + +The following import block imports the repositories able to access the actions organization variable named `myvariable` to a `github_actions_organization_variable_repositories` resource named `example`. + +```hcl +import { + to = github_actions_organization_variable_repositories.example + id = "myvariable" +} +``` + +### Import Command + +The following command imports the repositories able to access the actions organization variable named `myvariable` to a `github_actions_organization_variable_repositories` resource named `example`. + +```shell +terraform import github_actions_organization_variable_repositories.example myvariable +``` diff --git a/website/docs/r/actions_organization_variable_repository.html.markdown b/website/docs/r/actions_organization_variable_repository.html.markdown new file mode 100644 index 0000000000..7fc0992200 --- /dev/null +++ b/website/docs/r/actions_organization_variable_repository.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "github" +page_title: "GitHub: github_actions_organization_variable_repository" +description: |- + Add access for a repository to an Actions Variable within a GitHub organization. +--- + +# github_actions_organization_variable_repository + +This resource adds permission for a repository to use an actions variables within your GitHub organization. +You must have write access to an organization variable to use this resource. + +This resource is only applicable when `visibility` of the existing organization variable has been set to `selected`. + +## Example Usage + +```hcl +resource "github_actions_organization_variable" "example" { + variable_name = "myvariable" + plaintext_value = "foo" + visibility = "selected" +} + +resource "github_repository" "example" { + name = "myrepo" + visibility = "public" +} + +resource "github_actions_organization_variable_repository" "example" { + variable_name = github_actions_organization_variable.example.name + repository_id = github_repository.example.repo_id +} +``` + +## Argument Reference + +The following arguments are supported: + +- `variable_name` - (Required) Name of the actions organization variable. +- `repository_id` - (Required) ID of the repository that should be able to access the variable. + +## Import + +This resource can be imported using an ID made of the variable name and repository name separated by a `:`. + +### Import Block + +The following import block imports the access of repository ID `123456` for the actions organization variable named `myvariable` to a `github_actions_organization_variable_repository` resource named `example`. + +```hcl +import { + to = github_actions_organization_variable_repository.example + id = "myvariable:123456" +} +``` + +### Import Command + +The following command imports the access of repository ID `123456` for the actions organization variable named `myvariable` to a `github_actions_organization_variable_repository` resource named `example`. + +```shell +terraform import github_actions_organization_variable_repository.example myvariable:123456 +``` diff --git a/website/docs/r/dependabot_organization_secret_repositories.html.markdown b/website/docs/r/dependabot_organization_secret_repositories.html.markdown index f92ec2d8cd..aca52a612a 100644 --- a/website/docs/r/dependabot_organization_secret_repositories.html.markdown +++ b/website/docs/r/dependabot_organization_secret_repositories.html.markdown @@ -2,12 +2,12 @@ layout: "github" page_title: "GitHub: github_dependabot_organization_secret_repositories" description: |- - Manages repository allow list for an Dependabot Secret within a GitHub organization + Manages repository allow list for an Dependabot Secret within a GitHub organization. --- # github_dependabot_organization_secret_repositories -This resource allows you to manage the repository allow list for existing GitHub Dependabot secrets within your GitHub organization. +This resource allows you to manage the repositories allowed to access a Dependabot secret within your GitHub organization. You must have write access to an organization secret to use this resource. This resource is only applicable when `visibility` of the existing organization secret has been set to `selected`. @@ -15,19 +15,20 @@ This resource is only applicable when `visibility` of the existing organization ## Example Usage ```hcl -data "github_repository" "repo" { - full_name = "my-org/repo" +resource "github_dependabot_organization_secret" "example" { + secret_name = "mysecret" + plaintext_value = "foo" + visibility = "selected" } -resource "github_dependabot_organization_secret" "example_secret" { - secret_name = "example_secret_name" - visibility = "private" - plaintext_value = var.some_secret_string +resource "github_repository" "example" { + name = "myrepo" + visibility = "public" } -resource "github_dependabot_organization_secret_repositories" "org_secret_repos" { - secret_name = github_dependabot_organization_secret.example_secret.secret_name - selected_repository_ids = [data.github_repository.repo.repo_id] +resource "github_dependabot_organization_secret_repositories" "example" { + secret_name = github_dependabot_organization_secret.example.name + selected_repository_ids = [ github_repository.example.repo_id ] } ``` @@ -35,13 +36,28 @@ resource "github_dependabot_organization_secret_repositories" "org_secret_repos" The following arguments are supported: -* `secret_name` - (Required) Name of the existing secret -* `selected_repository_ids` - (Required) An array of repository ids that can access the organization secret. +- `secret_name` - (Required) Name of the Dependabot organization secret. +- `selected_repository_ids` - (Required) List of IDs for the repositories that should be able to access the secret. ## Import -This resource can be imported using an ID made up of the secret name: +This resource can be imported using the secret name as the ID. +### Import Block + +The following import block imports the repositories able to access the Dependabot organization secret named `mysecret` to a `github_dependabot_organization_secret_repositories` resource named `example`. + +```hcl +import { + to = github_dependabot_organization_secret_repositories.example + id = "mysecret" +} ``` -terraform import github_dependabot_organization_secret_repositories.test_secret_repos test_secret_name + +### Import Command + +The following command imports the repositories able to access the Dependabot organization secret named `mysecret` to a `github_dependabot_organization_secret_repositories` resource named `example`. + +```shell +terraform import github_dependabot_organization_secret_repositories.example mysecret ``` diff --git a/website/docs/r/dependabot_organization_secret_repository.html.markdown b/website/docs/r/dependabot_organization_secret_repository.html.markdown new file mode 100644 index 0000000000..1477ac64ad --- /dev/null +++ b/website/docs/r/dependabot_organization_secret_repository.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "github" +page_title: "GitHub: github_dependabot_organization_secret_repository" +description: |- + Add access for a repository to a Dependabot Secret within a GitHub organization. +--- + +# github_dependabot_organization_secret_repository + +This resource adds permission for a repository to use a Dependabot secret within your GitHub organization. +You must have write access to an organization secret to use this resource. + +This resource is only applicable when `visibility` of the existing organization secret has been set to `selected`. + +## Example Usage + +```hcl +resource "github_dependabot_organization_secret" "example" { + secret_name = "mysecret" + plaintext_value = "foo" + visibility = "selected" +} + +resource "github_repository" "example" { + name = "myrepo" + visibility = "public" +} + +resource "github_dependabot_organization_secret_repository" "example" { + secret_name = github_dependabot_organization_secret.example.name + repository_id = github_repository.example.repo_id +} +``` + +## Argument Reference + +The following arguments are supported: + +- `secret_name` - (Required) Name of the Dependabot organization secret. +- `repository_id` - (Required) ID of the repository that should be able to access the secret. + +## Import + +This resource can be imported using an ID made of the secret name and repository name separated by a `:`. + +### Import Block + +The following import block imports the access of repository ID `123456` for the Dependabot organization secret named `mysecret` to a `github_dependabot_organization_secret_repository` resource named `example`. + +```hcl +import { + to = github_dependabot_organization_secret_repository.example + id = "mysecret:123456" +} +``` + +### Import Command + +The following command imports the access of repository ID `123456` for the Dependabot organization secret named `mysecret` to a `v` resource named `example`. + +```shell +terraform import github_dependabot_organization_secret_repository.example mysecret:123456 +``` diff --git a/website/docs/r/emu_group_mapping.html.markdown b/website/docs/r/emu_group_mapping.html.markdown index e2819639e6..491515dcf1 100644 --- a/website/docs/r/emu_group_mapping.html.markdown +++ b/website/docs/r/emu_group_mapping.html.markdown @@ -9,8 +9,6 @@ description: |- This resource manages mappings between external groups for enterprise managed users and GitHub teams. It wraps the [Teams#ExternalGroups API](https://docs.github.com/en/rest/reference/teams#external-groups). Note that this is a distinct resource from `github_team_sync_group_mapping`. `github_emu_group_mapping` is special to the Enterprise Managed User (EMU) external group feature, whereas `github_team_sync_group_mapping` is specific to Identity Provider Groups. -!> **Warning:**: This resources `Read` function has a fundamental bug. It doesn't verify that the group is actually linked to the team. Someone could modify the linked group outside of Terraform and the resource would not detect it. - ## Example Usage ```hcl @@ -29,8 +27,8 @@ The following arguments are supported: ## Import -GitHub EMU External Group Mappings can be imported using the external `group_id` and `team_slug` separated by a colon, e.g. +GitHub EMU External Group Mappings can be imported using the `team_slug` and external `group_id` separated by a colon, e.g. ```sh -$ terraform import github_emu_group_mapping.example_emu_group_mapping 28836:emu-test-team +$ terraform import github_emu_group_mapping.example_emu_group_mapping emu-test-team:28836 ``` diff --git a/website/github.erb b/website/github.erb index 7db02fc5fc..997536b42f 100644 --- a/website/github.erb +++ b/website/github.erb @@ -89,13 +89,13 @@ github_codespaces_user_secrets
  • - dependabot_organization_public_key + github_dependabot_organization_public_key
  • - dependabot_organization_secrets + github_dependabot_organization_secrets
  • - dependabot_secrets + github_dependabot_secrets
  • github_enterprise @@ -224,25 +224,28 @@ Resources