diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 05bdc9cd4a..04ae67802b 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -92,6 +92,11 @@ func resourceGithubRepository() *schema.Resource { Optional: true, Default: false, }, + "topics": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, "full_name": { Type: schema.TypeString, @@ -137,6 +142,7 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { licenseTemplate := d.Get("license_template").(string) gitIgnoreTemplate := d.Get("gitignore_template").(string) archived := d.Get("archived").(bool) + topics := expandStringList(d.Get("topics").([]interface{})) repo := &github.Repository{ Name: &name, @@ -154,6 +160,7 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { LicenseTemplate: &licenseTemplate, GitignoreTemplate: &gitIgnoreTemplate, Archived: &archived, + Topics: topics, } return repo @@ -161,6 +168,7 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { func resourceGithubRepositoryCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*Organization).client + ctx := context.TODO() if _, ok := d.GetOk("default_branch"); ok { return fmt.Errorf("Cannot set the default branch on a new repository.") @@ -168,12 +176,20 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta interface{}) er repoReq := resourceGithubRepositoryObject(d) log.Printf("[DEBUG] create github repository %s/%s", meta.(*Organization).name, *repoReq.Name) - repo, _, err := client.Repositories.Create(context.TODO(), meta.(*Organization).name, repoReq) + repo, _, err := client.Repositories.Create(ctx, meta.(*Organization).name, repoReq) if err != nil { return err } d.SetId(*repo.Name) + topics := repoReq.Topics + if len(topics) > 0 { + _, _, err = client.Repositories.ReplaceAllTopics(ctx, meta.(*Organization).name, *repoReq.Name, topics) + if err != nil { + return err + } + } + return resourceGithubRepositoryUpdate(d, meta) } @@ -214,11 +230,13 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta interface{}) erro d.Set("git_clone_url", repo.GitURL) d.Set("http_clone_url", repo.CloneURL) d.Set("archived", repo.Archived) + d.Set("topics", flattenStringList(repo.Topics)) return nil } func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*Organization).client + ctx := context.TODO() repoReq := resourceGithubRepositoryObject(d) // Can only set `default_branch` on an already created repository with the target branches ref already in-place if v, ok := d.GetOk("default_branch"); ok { @@ -231,12 +249,20 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta interface{}) er repoName := d.Id() log.Printf("[DEBUG] update github repository %s/%s", meta.(*Organization).name, repoName) - repo, _, err := client.Repositories.Edit(context.TODO(), meta.(*Organization).name, repoName, repoReq) + repo, _, err := client.Repositories.Edit(ctx, meta.(*Organization).name, repoName, repoReq) if err != nil { return err } d.SetId(*repo.Name) + if d.HasChange("topics") { + topics := repoReq.Topics + _, _, err = client.Repositories.ReplaceAllTopics(ctx, meta.(*Organization).name, *repo.Name, topics) + if err != nil { + return err + } + } + return resourceGithubRepositoryRead(d, meta) } diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index 255b5501a8..3513869521 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -302,6 +302,75 @@ func TestAccGithubRepository_templates(t *testing.T) { }) } +func TestAccGithubRepository_topics(t *testing.T) { + var repo github.Repository + randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + name := fmt.Sprintf("tf-acc-test-%s", randString) + description := fmt.Sprintf("Terraform acceptance tests %s", randString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubRepositoryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubRepositoryConfigTopics(randString, `"topic1", "topic2"`), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubRepositoryExists("github_repository.foo", &repo), + testAccCheckGithubRepositoryAttributes(&repo, &testAccGithubRepositoryExpectedAttributes{ + Name: name, + Description: description, + Homepage: "http://example.com/", + Topics: []string{"topic1", "topic2"}, + + // non-zero defaults + DefaultBranch: "master", + AllowMergeCommit: true, + AllowSquashMerge: true, + AllowRebaseMerge: true, + }), + ), + }, + { + Config: testAccGithubRepositoryConfigTopics(randString, `"topic1", "topic2", "topic3"`), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubRepositoryExists("github_repository.foo", &repo), + testAccCheckGithubRepositoryAttributes(&repo, &testAccGithubRepositoryExpectedAttributes{ + Name: name, + Description: description, + Homepage: "http://example.com/", + Topics: []string{"topic1", "topic2", "topic3"}, + + // non-zero defaults + DefaultBranch: "master", + AllowMergeCommit: true, + AllowSquashMerge: true, + AllowRebaseMerge: true, + }), + ), + }, + { + Config: testAccGithubRepositoryConfigTopics(randString, ``), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubRepositoryExists("github_repository.foo", &repo), + testAccCheckGithubRepositoryAttributes(&repo, &testAccGithubRepositoryExpectedAttributes{ + Name: name, + Description: description, + Homepage: "http://example.com/", + Topics: []string{}, + + // non-zero defaults + DefaultBranch: "master", + AllowMergeCommit: true, + AllowSquashMerge: true, + AllowRebaseMerge: true, + }), + ), + }, + }, + }) +} + func testAccCheckGithubRepositoryExists(n string, repo *github.Repository) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -342,6 +411,7 @@ type testAccGithubRepositoryExpectedAttributes struct { LicenseTemplate string GitignoreTemplate string Archived bool + Topics []string } func testAccCheckGithubRepositoryAttributes(repo *github.Repository, want *testAccGithubRepositoryExpectedAttributes) resource.TestCheckFunc { @@ -377,7 +447,14 @@ func testAccCheckGithubRepositoryAttributes(repo *github.Repository, want *testA if *repo.HasDownloads != want.HasDownloads { return fmt.Errorf("got has downloads %#v; want %#v", *repo.HasDownloads, want.HasDownloads) } - + if len(want.Topics) != len(repo.Topics) { + return fmt.Errorf("got topics %#v; want %#v", repo.Topics, want.Topics) + } + for i := range want.Topics { + if repo.Topics[i] != want.Topics[i] { + return fmt.Errorf("got topics %#v; want %#v", repo.Topics, want.Topics) + } + } if *repo.DefaultBranch != want.DefaultBranch { return fmt.Errorf("got default branch %q; want %q", *repo.DefaultBranch, want.DefaultBranch) } @@ -628,3 +705,19 @@ resource "github_repository" "foo" { } `, randString, randString) } + +func testAccGithubRepositoryConfigTopics(randString string, topicList string) string { + return fmt.Sprintf(` +resource "github_repository" "foo" { + name = "tf-acc-test-%s" + description = "Terraform acceptance tests %s" + homepage_url = "http://example.com/" + + # So that acceptance tests can be run in a github organization + # with no billing + private = false + + topics = [%s] +} +`, randString, randString, topicList) +} diff --git a/github/util.go b/github/util.go index 6eb7600b82..8e9085e884 100644 --- a/github/util.go +++ b/github/util.go @@ -64,3 +64,22 @@ func validateTwoPartID(id string) error { func buildTwoPartID(a, b *string) string { return fmt.Sprintf("%s:%s", *a, *b) } + +func expandStringList(configured []interface{}) []string { + vs := make([]string, 0, len(configured)) + for _, v := range configured { + val, ok := v.(string) + if ok && val != "" { + vs = append(vs, val) + } + } + return vs +} + +func flattenStringList(v []string) []interface{} { + c := make([]interface{}, 0, len(v)) + for _, s := range v { + c = append(c, s) + } + return c +}