From 7daca1e374385acc812fb357deda668f2319d4c0 Mon Sep 17 00:00:00 2001 From: Alan Goldman Date: Mon, 31 Mar 2025 17:54:30 -0400 Subject: [PATCH] Adding a computed attribute for the repository owner in the `github_repository` resource and data source. Fixes #2503 --- github/data_source_github_repository.go | 12 +- github/data_source_github_repository_test.go | 175 +++++++++++++++++++ github/resource_github_repository.go | 8 + github/resource_github_repository_test.go | 47 +++++ website/docs/d/repository.html.markdown | 2 + website/docs/r/repository.html.markdown | 3 + 6 files changed, 246 insertions(+), 1 deletion(-) diff --git a/github/data_source_github_repository.go b/github/data_source_github_repository.go index 56972f89d5..f836f97bad 100644 --- a/github/data_source_github_repository.go +++ b/github/data_source_github_repository.go @@ -22,7 +22,7 @@ func dataSourceGithubRepository() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ConflictsWith: []string{"name"}, + ConflictsWith: []string{"name", "owner"}, }, "name": { Type: schema.TypeString, @@ -30,6 +30,13 @@ func dataSourceGithubRepository() *schema.Resource { Computed: true, ConflictsWith: []string{"full_name"}, }, + "owner": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"full_name"}, + Description: "Owner of the repository. If not provided, the provider's default owner is used.", + }, "description": { Type: schema.TypeString, Default: nil, @@ -360,6 +367,9 @@ func dataSourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, if name, ok := d.GetOk("name"); ok { repoName = name.(string) } + if ownerName, ok := d.GetOk("owner"); ok { + owner = ownerName.(string) + } if repoName == "" { return diag.Errorf("one of %q or %q has to be provided", "full_name", "name") diff --git a/github/data_source_github_repository_test.go b/github/data_source_github_repository_test.go index 515c639aab..b59053cac7 100644 --- a/github/data_source_github_repository_test.go +++ b/github/data_source_github_repository_test.go @@ -379,4 +379,179 @@ EOT }, }) }) + + t.Run("queries a repository using owner and name", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%s" + } + + data "github_repository" "test" { + name = github_repository.test.name + owner = "%s" + } + `, randomID, testOrganization) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.github_repository.test", "owner", + testOrganization, + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + testCase(t, anonymous) + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("validates conflicts between full_name, name, and owner", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + vulnerability_alerts = true + } + `, randomID) + + // Test invalid combinations + invalidConfigs := []string{ + // full_name with name + fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + vulnerability_alerts = true + } + + data "github_repository" "test" { + full_name = "%[2]s/tf-acc-%[1]s" + name = "tf-acc-%[1]s" + } + `, randomID, testOrganization), + // full_name with owner + fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + } + + data "github_repository" "test" { + full_name = "%[2]s/tf-acc-%[1]s" + owner = "%[2]s" + } + `, randomID, testOrganization), + // full_name with both name and owner + fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + } + + data "github_repository" "test" { + full_name = "%[2]s/tf-acc-%[1]s" + name = "tf-acc-%[1]s" + owner = "%[2]s" + } + `, randomID, testOrganization), + } + + // Test valid combinations + validConfigs := []string{ + // Just full_name + fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + } + + data "github_repository" "test" { + full_name = "%[2]s/tf-acc-%[1]s" + } + `, randomID, testOrganization), + // Just name (uses provider owner) + fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + } + + data "github_repository" "test" { + name = "tf-acc-%[1]s" + } + `, randomID), + // name with owner + fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-%[1]s" + } + + data "github_repository" "test" { + name = "tf-acc-%[1]s" + owner = "%[2]s" + } + `, randomID, testOrganization), + } + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create the repository first + { + Config: config, + }, + // Test that invalid configs fail + { + Config: invalidConfigs[0], + ExpectError: regexp.MustCompile("(?i)conflicts with"), + }, + { + Config: invalidConfigs[1], + ExpectError: regexp.MustCompile("(?i)conflicts with"), + }, + { + Config: invalidConfigs[2], + ExpectError: regexp.MustCompile("(?i)conflicts with"), + }, + // Test that valid configs succeed + { + Config: validConfigs[0], + }, + { + Config: validConfigs[1], + }, + { + Config: validConfigs[2], + }, + }, + }) + } + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) } diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 9d756ac6c9..497f50db93 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -412,6 +412,11 @@ func resourceGithubRepository() *schema.Resource { Computed: true, Description: "A string of the form 'orgname/reponame'.", }, + "owner": { + Type: schema.TypeString, + Computed: true, + Description: "The owner of the repository.", + }, "html_url": { Type: schema.TypeString, Computed: true, @@ -832,6 +837,9 @@ func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, m _ = d.Set("topics", flattenStringList(repo.Topics)) _ = d.Set("node_id", repo.GetNodeID()) _ = d.Set("repo_id", repo.GetID()) + if repo.Owner != nil { + _ = d.Set("owner", repo.Owner.GetLogin()) + } // TODO: Validate this behavior as I can see these fields being returned even when archived // GitHub API doesn't respond following parameters when repository is archived diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index 1589c84bf1..a4af451a37 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -894,6 +894,53 @@ resource "github_repository" "test" { // }) // }) + t.Run("creates repository and returns owner field", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-owner-%[1]s" + description = "Terraform acceptance tests %[1]s" + } + `, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_repository.test", "owner", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + +} + +func TestAccGithubRepositoryPages(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + t.Run("manages the legacy pages feature for a repository", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) testRepoName := fmt.Sprintf("%slegacy-pages-%s", testResourcePrefix, randomID) diff --git a/website/docs/d/repository.html.markdown b/website/docs/d/repository.html.markdown index 071da23c89..84ad79d160 100644 --- a/website/docs/d/repository.html.markdown +++ b/website/docs/d/repository.html.markdown @@ -25,6 +25,8 @@ The following arguments are supported: * `full_name` - (Optional) Full name of the repository (in `org/name` format). +* `owner` - (Optional) Owner of the repository. If not provided, the provider's default owner is used. + ## Attributes Reference * `node_id` - the Node ID of the repository. diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 864c2596a2..ad73973de2 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -222,6 +222,8 @@ The following additional attributes are exported: * `full_name` - A string of the form "orgname/reponame". +* `owner` - The owner of the repository. + * `html_url` - URL to the repository on the web. * `ssh_clone_url` - URL that can be provided to `git clone` to clone the repository via SSH. @@ -250,3 +252,4 @@ Repositories can be imported using the `name`, e.g. ```shell terraform import github_repository.terraform myrepo ``` +