From d6a569cb07362af8443e8682f08a66417cd3263c Mon Sep 17 00:00:00 2001 From: Jeremy Udit Date: Thu, 19 Dec 2019 13:01:42 -0500 Subject: [PATCH 1/3] resource/repository: add create from template --- github/resource_github_repository.go | 84 +++++++++++++++++++++-- github/resource_github_repository_test.go | 69 +++++++++++++++++++ go.mod | 2 + website/docs/r/repository.html.markdown | 15 +++- 4 files changed, 162 insertions(+), 8 deletions(-) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 3d13d6e97f..107f930961 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "fmt" "log" "net/http" @@ -137,6 +138,25 @@ func resourceGithubRepository() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "template": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "owner": { + Type: schema.TypeString, + Optional: true, + Default: false, + }, + "repository": { + Type: schema.TypeString, + Optional: true, + Default: false, + }, + }, + }, + }, }, } } @@ -174,20 +194,58 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Cannot set the default branch on a new repository to something other than 'master'.") } - orgName := meta.(*Organization).name repoReq := resourceGithubRepositoryObject(d) + orgName := meta.(*Organization).name + repoName := repoReq.GetName() ctx := context.Background() - log.Printf("[DEBUG] Creating repository: %s/%s", orgName, repoReq.GetName()) - repo, _, err := client.Repositories.Create(ctx, orgName, repoReq) - if err != nil { - return err + log.Printf("[DEBUG] Creating repository: %s/%s", orgName, repoName) + + if template, ok := d.GetOk("template"); ok { + templateConfigBlocks := template.([]interface{}) + if len(templateConfigBlocks) > 1 { + return errors.New("cannot specify template more than once") + } + + for _, templateConfigBlock := range templateConfigBlocks { + templateConfigMap, ok := templateConfigBlock.(map[string]interface{}) + if !ok { + return errors.New("failed to unpack template configuration block") + } + + templateRepo := templateConfigMap["repository"].(string) + templateRepoOwner := templateConfigMap["owner"].(string) + templateRepoReq := github.TemplateRepoRequest{ + Name: &repoName, + Owner: &orgName, + Description: github.String(d.Get("description").(string)), + Private: github.Bool(d.Get("private").(bool)), + } + + repo, _, err := client.Repositories.CreateFromTemplate(ctx, + templateRepoOwner, + templateRepo, + &templateRepoReq, + ) + + if err != nil { + return err + } + + d.SetId(*repo.Name) + } + } else { + // Create without a repository template + repo, _, err := client.Repositories.Create(ctx, orgName, repoReq) + if err != nil { + return err + } + d.SetId(*repo.Name) } - d.SetId(*repo.Name) topics := repoReq.Topics if len(topics) > 0 { - _, _, err = client.Repositories.ReplaceAllTopics(ctx, orgName, repoReq.GetName(), topics) + _, _, err = client.Repositories.ReplaceAllTopics(ctx, orgName, repoName, topics) if err != nil { return err } @@ -250,6 +308,18 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta interface{}) erro d.Set("http_clone_url", repo.CloneURL) d.Set("archived", repo.Archived) d.Set("topics", flattenStringList(repo.Topics)) + + if repo.TemplateRepository != nil { + d.Set("template", []interface{}{ + map[string]interface{}{ + "owner": repo.TemplateRepository.Owner.Login, + "repository": repo.TemplateRepository.Name, + }, + }) + } else { + d.Set("template", []interface{}{}) + } + return nil } diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index b18e8926cd..622f8a172e 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -490,6 +490,36 @@ func TestAccGithubRepository_autoInitForceNew(t *testing.T) { }) } +func TestAccGithubRepository_createFromTemplate(t *testing.T) { + var repo github.Repository + + rn := "github_repository.foo" + randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubRepositoryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubRepositoryCreateFromTemplate(randString), + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubRepositoryExists(rn, &repo), + testAccCheckGithubRepositoryTemplateRepoAttribute(rn, &repo), + ), + }, + { + ResourceName: rn, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "auto_init", + }, + }, + }, + }) +} + func testAccCheckGithubRepositoryExists(n string, repo *github.Repository) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -513,6 +543,17 @@ func testAccCheckGithubRepositoryExists(n string, repo *github.Repository) resou } } +func testAccCheckGithubRepositoryTemplateRepoAttribute(n string, repo *github.Repository) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if *repo.TemplateRepository.IsTemplate != true { + return fmt.Errorf("got repo %q; want %q", *repo.TemplateRepository, repo) + } + + return nil + } +} + type testAccGithubRepositoryExpectedAttributes struct { Name string Description string @@ -841,6 +882,34 @@ resource "github_repository" "foo" { `, randString, randString) } +func testAccGithubRepositoryCreateFromTemplate(randString string) string { + return fmt.Sprintf(` +resource "github_repository" "foo" { + name = "tf-acc-test-%s" + description = "Terraform acceptance tests %s" + homepage_url = "http://example.com/" + + template { + # FIXME: Change this to something more suitable for CI runs + owner = "jcudit" + repository = "terraform-template-module" + } + + # So that acceptance tests can be run in a github organization + # with no billing + private = false + + has_issues = true + has_wiki = true + allow_merge_commit = true + allow_squash_merge = false + allow_rebase_merge = false + has_downloads = true + +} +`, randString, randString) +} + func testAccGithubRepositoryConfigTopics(randString string, topicList string) string { return fmt.Sprintf(` resource "github_repository" "foo" { diff --git a/go.mod b/go.mod index 10eb37901d..c7b3b44a52 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( github.com/terraform-providers/terraform-provider-tls v1.2.0 golang.org/x/oauth2 v0.0.0-20190604054615-0f29369cfe45 ) + +go 1.13 diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 28723258b5..5447db65be 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -21,6 +21,11 @@ resource "github_repository" "example" { description = "My awesome codebase" private = true + + template { + owner = "github" + repo = "terraform-module-template" + } } ``` @@ -65,10 +70,18 @@ and after a correct reference has been created for the target branch inside the initial repository creation and create the target branch inside of the repository prior to setting this attribute. * `archived` - (Optional) Specifies if the repository should be archived. Defaults to `false`. +~> **NOTE** Currently, the API does not support unarchiving. * `topics` - (Optional) The list of topics of the repository. -~> **NOTE** Currently, the API does not support unarchiving. +* `template` - (Optional) Use a template repository to create this resource. See [Template Repositories](#template-repositories) below for details. + +### Template Repositories + +`template` supports the following arguments: + +* `owner`: The GitHub organization or user the template repository is owned by. +* `repository`: The name of the template repository. ## Attributes Reference From 8d098c19efca7b5dc7118a27ec260065e0be49cd Mon Sep 17 00:00:00 2001 From: Jeremy Udit Date: Mon, 6 Jan 2020 20:47:57 -0500 Subject: [PATCH 2/3] resource/repository: create from template fixes * Create a new resource if `template` changes * Make required fields non-optional * Remove unnecessary length check * Defer bump to go 1.13 * Replace hard-coded test values with env vars --- github/provider_test.go | 3 +++ github/resource_github_repository.go | 10 +++------- github/resource_github_repository_test.go | 11 +++++++---- go.mod | 2 -- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/github/provider_test.go b/github/provider_test.go index 899484eb3f..b97f87f646 100644 --- a/github/provider_test.go +++ b/github/provider_test.go @@ -64,6 +64,9 @@ func testAccPreCheck(t *testing.T) { if v := os.Getenv("GITHUB_TEST_COLLABORATOR"); v == "" { t.Fatal("GITHUB_TEST_COLLABORATOR must be set for acceptance tests") } + if v := os.Getenv("GITHUB_TEMPLATE_REPOSITORY"); v == "" { + t.Fatal("GITHUB_TEMPLATE_REPOSITORY must be set for acceptance tests") + } } func TestProvider_individual(t *testing.T) { diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 107f930961..a0a9929d29 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -141,18 +141,17 @@ func resourceGithubRepository() *schema.Resource { "template": { Type: schema.TypeList, Optional: true, + ForceNew: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "owner": { Type: schema.TypeString, - Optional: true, - Default: false, + Required: true, }, "repository": { Type: schema.TypeString, - Optional: true, - Default: false, + Required: true, }, }, }, @@ -203,9 +202,6 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta interface{}) er if template, ok := d.GetOk("template"); ok { templateConfigBlocks := template.([]interface{}) - if len(templateConfigBlocks) > 1 { - return errors.New("cannot specify template more than once") - } for _, templateConfigBlock := range templateConfigBlocks { templateConfigMap, ok := templateConfigBlock.(map[string]interface{}) diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index 622f8a172e..3ca4fabda7 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -883,6 +883,10 @@ resource "github_repository" "foo" { } func testAccGithubRepositoryCreateFromTemplate(randString string) string { + + owner := os.Getenv("GITHUB_ORGANIZATION") + repository := os.Getenv("GITHUB_TEMPLATE_REPOSITORY") + return fmt.Sprintf(` resource "github_repository" "foo" { name = "tf-acc-test-%s" @@ -890,9 +894,8 @@ resource "github_repository" "foo" { homepage_url = "http://example.com/" template { - # FIXME: Change this to something more suitable for CI runs - owner = "jcudit" - repository = "terraform-template-module" + owner = "%s" + repository = "%s" } # So that acceptance tests can be run in a github organization @@ -907,7 +910,7 @@ resource "github_repository" "foo" { has_downloads = true } -`, randString, randString) +`, randString, randString, owner, repository) } func testAccGithubRepositoryConfigTopics(randString string, topicList string) string { diff --git a/go.mod b/go.mod index c7b3b44a52..10eb37901d 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,3 @@ require ( github.com/terraform-providers/terraform-provider-tls v1.2.0 golang.org/x/oauth2 v0.0.0-20190604054615-0f29369cfe45 ) - -go 1.13 From f9fc68b63f9d87dcbf9ed7044df9a8236c8dc35a Mon Sep 17 00:00:00 2001 From: Jeremy Udit Date: Thu, 9 Jan 2020 20:52:02 -0500 Subject: [PATCH 3/3] resource/repository: align README with new requirement --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8a2c72c031..3f2228bd60 100644 --- a/README.md +++ b/README.md @@ -84,18 +84,22 @@ testing. It will need to have the following scopes selected: Once the token has been created, it must be exported in your environment as `GITHUB_TOKEN`. ### GitHub organization -If you do not have an organization already that you are comfortable running tests again, you will need to [create one](https://help.github.com/en/articles/creating-a-new-organization-from-scratch). The free "Team for Open Source" org type is fine for these tests. The name of the +If you do not have an organization already that you are comfortable running tests against, you will need to [create one](https://help.github.com/en/articles/creating-a-new-organization-from-scratch). The free "Team for Open Source" org type is fine for these tests. The name of the organization must then be exported in your environment as `GITHUB_ORGANIZATION`. -### Test repository -In the organization you are using above, create a test repository named `test-repo`. Make sure the repository is configured as follows: -* The description should be `Test description, used in GitHub Terraform provider acceptance test.` -* The website url should be `http://www.example.com` -* Create two topics within the repo named `test-topic` and `second-test-topic` -* In the repo settings, make sure all features and merge button options are enabled. +### Test repositories +In the organization you are using above, create the following test repositories: + +* `test-repo` + * The description should be `Test description, used in GitHub Terraform provider acceptance test.` + * The website url should be `http://www.example.com` + * Create two topics within the repo named `test-topic` and `second-test-topic` + * In the repo settings, make sure all features and merge button options are enabled. +* `test-repo-template` + * Configure the repository to be a [Template repository](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository) ### GitHub users Export your github username (the one you used to create the personal access token above) as `GITHUB_TEST_USER`. You will need to export a different github username as `GITHUB_TEST_COLLABORATOR`. Please note that these usernames cannot be the same as each other, and both of them must be real github usernames. The collaborator user does not need to be added as a collaborator to your test repo or organization, but as -the acceptance tests do real things (and will trigger some notifications for this user), you should probably make sure the person you specify knows that you're doing this just to be nice. \ No newline at end of file +the acceptance tests do real things (and will trigger some notifications for this user), you should probably make sure the person you specify knows that you're doing this just to be nice.