diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index a3b139cade..f3cea8f5f6 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -3,23 +3,26 @@ package github import ( "context" "errors" - "log" "net/http" "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubBranchDefault() *schema.Resource { return &schema.Resource{ - Create: resourceGithubBranchDefaultCreate, - Read: resourceGithubBranchDefaultRead, - Delete: resourceGithubBranchDefaultDelete, - Update: resourceGithubBranchDefaultUpdate, + CreateContext: resourceGithubBranchDefaultCreate, + ReadContext: resourceGithubBranchDefaultRead, + UpdateContext: resourceGithubBranchDefaultUpdate, + DeleteContext: resourceGithubBranchDefaultDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubBranchDefaultImport, }, + CustomizeDiff: diffRepository, + Schema: map[string]*schema.Schema{ "branch": { Type: schema.TypeString, @@ -29,9 +32,13 @@ func resourceGithubBranchDefault() *schema.Resource { "repository": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "The GitHub repository.", }, + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The GitHub repository ID.", + }, "rename": { Type: schema.TypeBool, Optional: true, @@ -51,47 +58,80 @@ func resourceGithubBranchDefault() *schema.Resource { } } -func resourceGithubBranchDefaultCreate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name +func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + repoName := d.Get("repository").(string) defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) - ctx := context.Background() + tflog.Trace(ctx, "Creating default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + "branch": defaultBranch, + "rename": rename, + }) - repository, _, err := client.Repositories.Get(ctx, owner, repoName) + repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } - if *repository.DefaultBranch != defaultBranch { + tflog.Debug(ctx, "Fetched repository", map[string]any{ + "current_default_branch": repository.GetDefaultBranch(), + }) + + if repository.GetDefaultBranch() != defaultBranch { if rename { - if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, *repository.DefaultBranch, defaultBranch); err != nil { - return err + tflog.Debug(ctx, "Renaming branch to new default") + if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { + return diag.FromErr(err) } } else { + tflog.Debug(ctx, "Setting new default branch") repository := &github.Repository{ - DefaultBranch: &defaultBranch, + DefaultBranch: github.Ptr(defaultBranch), } if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { - return err + return diag.FromErr(err) } } + } else { + tflog.Debug(ctx, "Default branch already set to desired branch, skipping update") } d.SetId(repoName) - return resourceGithubBranchDefaultRead(d, meta) + if err := d.Set("repository_id", int(repository.GetID())); err != nil { + return diag.FromErr(err) + } + if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, "Finished creating default branch resource", map[string]any{ + "resource_id": d.Id(), + }) + + return nil } -func resourceGithubBranchDefaultRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - repoName := d.Id() +func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "resource_id", d.Id()) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := d.Get("repository").(string) + + tflog.Trace(ctx, "Reading default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + }) - ctx := context.WithValue(context.Background(), ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -101,70 +141,123 @@ func resourceGithubBranchDefaultRead(d *schema.ResourceData, meta any) error { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotModified { + tflog.Debug(ctx, "Repository not modified, skipping read") return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing repository %s/%s from state because it no longer exists in GitHub", - owner, repoName) + tflog.Info(ctx, "Removing repository from state because it no longer exists in GitHub", map[string]any{ + "owner": owner, + "repository": repoName, + }) d.SetId("") return nil } } - return err + return diag.FromErr(err) } if repository.DefaultBranch == nil { + tflog.Warn(ctx, "Default branch is nil, removing resource from state") d.SetId("") return nil } - _ = d.Set("etag", resp.Header.Get("ETag")) - _ = d.Set("branch", *repository.DefaultBranch) - _ = d.Set("repository", *repository.Name) - return nil -} - -func resourceGithubBranchDefaultDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - repoName := d.Id() + if err := d.Set("repository_id", int(repository.GetID())); err != nil { + return diag.FromErr(err) + } - repository := &github.Repository{ - DefaultBranch: nil, + if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) } - ctx := context.Background() + if err := d.Set("branch", repository.GetDefaultBranch()); err != nil { + return diag.FromErr(err) + } - _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) - return err + tflog.Trace(ctx, "Finished reading default branch resource") + return nil } -func resourceGithubBranchDefaultUpdate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - repoName := d.Id() +func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "resource_id", d.Id()) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := d.Get("repository").(string) defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) - ctx := context.Background() + tflog.Trace(ctx, "Updating default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + "branch": defaultBranch, + "rename": rename, + }) + + var etag string if rename { - repository, _, err := client.Repositories.Get(ctx, owner, repoName) + tflog.Debug(ctx, "Renaming branch to new default") + repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } - if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, *repository.DefaultBranch, defaultBranch); err != nil { - return err + etag = resp.Header.Get("ETag") + if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { + return diag.FromErr(err) } } else { + tflog.Debug(ctx, "Setting new default branch") repository := &github.Repository{ - DefaultBranch: &defaultBranch, + DefaultBranch: github.Ptr(defaultBranch), } - if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { - return err + if _, resp, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { + return diag.FromErr(err) + } else { + etag = resp.Header.Get("ETag") } } + if err := d.Set("etag", etag); err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, "Finished updating default branch resource") + return nil +} + +func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "resource_id", d.Id()) + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + repoName := d.Get("repository").(string) + + tflog.Trace(ctx, "Deleting default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + }) + + repository := &github.Repository{ + DefaultBranch: nil, + } + + _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) + if err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, "Finished deleting default branch resource") + return nil +} + +func resourceGithubBranchDefaultImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + repoName := d.Id() + + if err := d.Set("repository", repoName); err != nil { + return nil, err + } - return resourceGithubBranchDefaultRead(d, meta) + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_branch_default_test.go b/github/resource_github_branch_default_test.go index 47855e32b4..0b993912cb 100644 --- a/github/resource_github_branch_default_test.go +++ b/github/resource_github_branch_default_test.go @@ -4,14 +4,19 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubBranchDefault(t *testing.T) { - t.Run("creates and manages branch defaults", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_as_import_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { @@ -25,32 +30,26 @@ func TestAccGithubBranchDefault(t *testing.T) { } `, repoName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_branch_default.test", "branch", - "main", - ), - resource.TestCheckResourceAttr( - "github_branch_default.test", "repository", - repoName, - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("branch"), "github_repository.test", tfjsonpath.New("default_branch"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, }, }) }) - t.Run("replaces the default_branch of a repository", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_default_branch_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -69,20 +68,18 @@ func TestAccGithubBranchDefault(t *testing.T) { `, repoName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_branch_default.test", "branch", - "test", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("branch"), "github_branch.test", tfjsonpath.New("branch"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, { Config: ` @@ -96,9 +93,9 @@ func TestAccGithubBranchDefault(t *testing.T) { }) }) - t.Run("creates and manages branch defaults even if rename is set", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_as_import_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -112,16 +109,38 @@ func TestAccGithubBranchDefault(t *testing.T) { } `, repoName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_branch_default.test", "branch", - "main", - ), - resource.TestCheckResourceAttr( - "github_branch_default.test", "repository", - repoName, - ), - ) + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("branch"), "github_repository.test", tfjsonpath.New("default_branch"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, + }, + }, + }) + }) + + t.Run("creates_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test"{ + repository = github_repository.test.name + branch = "development" + rename = true + } + `, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, @@ -129,15 +148,122 @@ func TestAccGithubBranchDefault(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, }, }) }) - t.Run("replaces the default_branch of a repository without creating a branch resource prior to", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("updates_default_branch_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + config := ` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + resource "github_branch" "test" { + repository = github_repository.test.name + branch = "test" + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "%s" + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, "main"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "github_branch_default.test", tfjsonpath.New("branch"), + "github_repository.test", tfjsonpath.New("default_branch"), + compare.ValuesSame(), + ), + }, + }, + { + Config: fmt.Sprintf(config, repoName, "test"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_branch_default.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("test")), + }, + }, + { + Config: ` + removed { + from = github_branch.test + lifecycle { destroy = false } + } + `, + }, + }, + }) + }) + + t.Run("updates_default_branch_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + config := ` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "%s" + rename = true + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, "main"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "github_branch_default.test", tfjsonpath.New("branch"), + "github_repository.test", tfjsonpath.New("default_branch"), + compare.ValuesSame(), + ), + }, + }, + { + Config: fmt.Sprintf(config, repoName, "development"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_branch_default.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + }, + }) + }) + t.Run("imports_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -157,7 +283,15 @@ func TestAccGithubBranchDefault(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc(resource.TestCheckResourceAttr("github_branch_default.test", "branch", "development")), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + { + ResourceName: "github_branch_default.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"rename", "etag"}, }, }, }) diff --git a/website/docs/r/branch_default.html.markdown b/website/docs/r/branch_default.html.markdown index e7d4361046..f5763d299c 100644 --- a/website/docs/r/branch_default.html.markdown +++ b/website/docs/r/branch_default.html.markdown @@ -9,9 +9,9 @@ description: |- Provides a GitHub branch default resource. -This resource allows you to set the default branch for a given repository. +This resource allows you to set the default branch for a given repository. -Note that use of this resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. +Note that use of this resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. ## Example Usage @@ -55,14 +55,20 @@ resource "github_branch_default" "default"{ The following arguments are supported: -* `repository` - (Required) The GitHub repository -* `branch` - (Required) The branch (e.g. `main`) -* `rename` - (Optional) Indicate if it should rename the branch rather than use an existing branch. Defaults to `false`. +- `repository` - (Required) The GitHub repository +- `branch` - (Required) The branch (e.g. `main`) +- `rename` - (Optional) Indicate if it should rename the branch rather than use an existing branch. Defaults to `false`. + +## Attribute Reference + +The following attributes are exported: + +- `repository_id` - The GitHub repository ID. ## Import -GitHub Branch Defaults can be imported using an ID made up of `repository`, e.g. +GitHub Branch Defaults can be imported using the repository name, e.g. -``` -$ terraform import github_branch_default.branch_default my-repo +```text +terraform import github_branch_default.branch_default my-repo ```