diff --git a/github/resource_github_repository_autolink_reference.go b/github/resource_github_repository_autolink_reference.go index 110a1fc119..75a8189318 100644 --- a/github/resource_github_repository_autolink_reference.go +++ b/github/resource_github_repository_autolink_reference.go @@ -4,22 +4,24 @@ import ( "context" "errors" "fmt" - "log" "net/http" "regexp" "strconv" "strings" "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" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceGithubRepositoryAutolinkReference() *schema.Resource { return &schema.Resource{ - Create: resourceGithubRepositoryAutolinkReferenceCreate, - Read: resourceGithubRepositoryAutolinkReferenceRead, - Delete: resourceGithubRepositoryAutolinkReferenceDelete, + CreateContext: resourceGithubRepositoryAutolinkReferenceCreate, + UpdateContext: resourceGithubRepositoryAutolinkReferenceUpdate, + ReadContext: resourceGithubRepositoryAutolinkReferenceRead, + DeleteContext: resourceGithubRepositoryAutolinkReferenceDelete, Importer: &schema.ResourceImporter{ StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { @@ -31,15 +33,15 @@ func resourceGithubRepositoryAutolinkReference() *schema.Resource { repository := parts[0] id := parts[1] + client := meta.(*Owner).v3client + owner := meta.(*Owner).name + // If the second part of the provided ID isn't an integer, assume that the // caller provided the key prefix for the autolink reference, and look up // the autolink by the key prefix. _, err := strconv.Atoi(id) if err != nil { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - autolink, err := getAutolinkByKeyPrefix(ctx, client, owner, repository, id) if err != nil { return nil, err @@ -48,22 +50,38 @@ func resourceGithubRepositoryAutolinkReference() *schema.Resource { id = strconv.FormatInt(*autolink.ID, 10) } + d.SetId(id) + + repo, _, err := client.Repositories.Get(ctx, owner, repository) + if err != nil { + return nil, fmt.Errorf("failed to retrieve repository %s: %w", repository, err) + } + if err = d.Set("repository", repository); err != nil { return nil, err } - d.SetId(id) + if err = d.Set("repository_id", int(repo.GetID())); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil }, }, + CustomizeDiff: diffRepository, + SchemaVersion: 1, Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "The repository name", }, + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the GitHub repository.", + }, "key_prefix": { Type: schema.TypeString, Required: true, @@ -92,15 +110,15 @@ func resourceGithubRepositoryAutolinkReference() *schema.Resource { } } -func resourceGithubRepositoryAutolinkReferenceCreate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client +func resourceGithubRepositoryAutolinkReferenceCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - owner := meta.(*Owner).name repoName := d.Get("repository").(string) keyPrefix := d.Get("key_prefix").(string) targetURLTemplate := d.Get("target_url_template").(string) isAlphanumeric := d.Get("is_alphanumeric").(bool) - ctx := context.Background() opts := &github.AutolinkOptions{ KeyPrefix: &keyPrefix, @@ -110,23 +128,35 @@ func resourceGithubRepositoryAutolinkReferenceCreate(d *schema.ResourceData, met autolinkRef, _, err := client.Repositories.AddAutolink(ctx, owner, repoName, opts) if err != nil { - return err + return diag.FromErr(err) } d.SetId(strconv.FormatInt(autolinkRef.GetID(), 10)) - return resourceGithubRepositoryAutolinkReferenceRead(d, meta) + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("repository_id", int(repo.GetID())); err != nil { + return diag.FromErr(err) + } + + return resourceGithubRepositoryAutolinkReferenceRead(ctx, d, m) } -func resourceGithubRepositoryAutolinkReferenceRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client +func resourceGithubRepositoryAutolinkReferenceRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "id", d.Id()) + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - owner := meta.(*Owner).name repoName := d.Get("repository").(string) autolinkRefID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -136,44 +166,60 @@ func resourceGithubRepositoryAutolinkReferenceRead(d *schema.ResourceData, meta var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing autolink reference for repository %s/%s from state because it no longer exists in GitHub", - owner, repoName) + tflog.Info(ctx, "Autolink reference not found, removing from state.", map[string]any{ + "owner": owner, + "repository": repoName, + }) d.SetId("") return nil } } - return err + return diag.FromErr(err) } - // Set resource fields d.SetId(strconv.FormatInt(autolinkRef.GetID(), 10)) if err = d.Set("repository", repoName); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("key_prefix", autolinkRef.KeyPrefix); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("target_url_template", autolinkRef.URLTemplate); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("is_alphanumeric", autolinkRef.IsAlphanumeric); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubRepositoryAutolinkReferenceDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client +func resourceGithubRepositoryAutolinkReferenceUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + tflog.Warn(ctx, "Update function of autolink reference. This should not be called. But it's necessary when 'repository' doesn't have `ForceNew`", map[string]any{ + "repository": d.Get("repository"), + "repository_id": d.Get("repository_id"), + "id": d.Id(), + }) + return nil +} + +func resourceGithubRepositoryAutolinkReferenceDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "id", d.Id()) + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - owner := meta.(*Owner).name repoName := d.Get("repository").(string) autolinkRefID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) _, err = client.Repositories.DeleteAutolink(ctx, owner, repoName, autolinkRefID) - return err + if err != nil { + return diag.FromErr(err) + } + + return nil } diff --git a/github/resource_github_repository_autolink_reference_test.go b/github/resource_github_repository_autolink_reference_test.go index e97b14abf6..b014f7faa4 100644 --- a/github/resource_github_repository_autolink_reference_test.go +++ b/github/resource_github_repository_autolink_reference_test.go @@ -5,8 +5,13 @@ import ( "regexp" "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 TestAccGithubRepositoryAutolinkReference(t *testing.T) { @@ -50,56 +55,34 @@ func TestAccGithubRepositoryAutolinkReference(t *testing.T) { } `, repoName) - check := resource.ComposeTestCheckFunc( - // autolink_default - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_default", "key_prefix", "TEST1-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_default", "target_url_template", "https://example.com/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_default", "is_alphanumeric", "true", - ), - // autolink_alphanumeric - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_alphanumeric", "key_prefix", "TEST2-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_alphanumeric", "target_url_template", "https://example.com/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_alphanumeric", "is_alphanumeric", "true", - ), - // autolink_numeric - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_numeric", "key_prefix", "TEST3-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_numeric", "target_url_template", "https://example.com/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_numeric", "is_alphanumeric", "false", - ), - // autolink_with_port - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_with_port", "key_prefix", "TEST4-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_with_port", "target_url_template", "https://example.com:8443/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_with_port", "is_alphanumeric", "true", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + // autolink_default + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST1-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + // autolink_alphanumeric + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST2-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + // autolink_numeric + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST3-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + // autolink_with_port + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST4-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com:8443/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, }, }, }) @@ -145,56 +128,34 @@ func TestAccGithubRepositoryAutolinkReference(t *testing.T) { } `, repoName) - check := resource.ComposeTestCheckFunc( - // autolink_default - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_default", "key_prefix", "TEST1-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_default", "target_url_template", "https://example.com/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_default", "is_alphanumeric", "true", - ), - // autolink_alphanumeric - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_alphanumeric", "key_prefix", "TEST2-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_alphanumeric", "target_url_template", "https://example.com/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_alphanumeric", "is_alphanumeric", "true", - ), - // autolink_numeric - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_numeric", "key_prefix", "TEST3-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_numeric", "target_url_template", "https://example.com/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_numeric", "is_alphanumeric", "false", - ), - // autolink_with_port - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_with_port", "key_prefix", "TEST4-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_with_port", "target_url_template", "https://example.com:8443/TEST-", - ), - resource.TestCheckResourceAttr( - "github_repository_autolink_reference.autolink_with_port", "is_alphanumeric", "true", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + // autolink_default + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST1-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + // autolink_alphanumeric + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST2-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_alphanumeric", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + // autolink_numeric + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST3-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_numeric", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + // autolink_with_port + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("key_prefix"), knownvalue.StringExact("TEST4-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("target_url_template"), knownvalue.StringExact("https://example.com:8443/TEST-")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("is_alphanumeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_with_port", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + }, }, // autolink_default { @@ -296,4 +257,48 @@ func TestAccGithubRepositoryAutolinkReference(t *testing.T) { }, }) }) + t.Run("should not recreate autolink reference when repository is renamed", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%srepo-rename-%s", testResourcePrefix, randomID) + repoNameRenamed := fmt.Sprintf("%srepo-renamed-%s", testResourcePrefix, randomID) + const configStr = ` + resource "github_repository" "test" { + name = "%s" + description = "Test autolink creation" + } + + resource "github_repository_autolink_reference" "autolink_default" { + repository = github_repository.test.name + + key_prefix = "TEST1-" + target_url_template = "https://example.com/TEST-" + } +` + + repoIdChangeCheck := statecheck.CompareValue(compare.ValuesSame()) + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configStr, repoName), + ConfigStateChecks: []statecheck.StateCheck{ + repoIdChangeCheck.AddStateValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("repository_id")), + }, + }, + { + Config: fmt.Sprintf(configStr, repoNameRenamed), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_repository_autolink_reference.autolink_default", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + repoIdChangeCheck.AddStateValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("repository_id")), + statecheck.ExpectKnownValue("github_repository_autolink_reference.autolink_default", tfjsonpath.New("repository"), knownvalue.StringExact(repoNameRenamed)), + }, + }, + }, + }) + }) } diff --git a/website/docs/r/repository_autolink_reference.html.markdown b/website/docs/r/repository_autolink_reference.html.markdown index 1fbebc5b22..9fafc178db 100644 --- a/website/docs/r/repository_autolink_reference.html.markdown +++ b/website/docs/r/repository_autolink_reference.html.markdown @@ -13,10 +13,9 @@ This resource allows you to create and manage an autolink reference for a single ```hcl resource "github_repository" "repo" { - name = "my-repo" - description = "GitHub repo managed by Terraform" - - private = false + name = "my-repo" + description = "GitHub repo managed by Terraform" + visibility = "public" } resource "github_repository_autolink_reference" "autolink" { @@ -32,19 +31,20 @@ resource "github_repository_autolink_reference" "autolink" { The following arguments are supported: -* `repository` - (Required) The repository of the autolink reference. +- `repository` - (Required) The repository of the autolink reference. If the repository is renamed, the autolink reference will be updated in-place rather than recreated. -* `key_prefix` - (Required) This prefix appended by a number will generate a link any time it is found in an issue, pull request, or commit. +- `key_prefix` - (Required) This prefix appended by a number will generate a link any time it is found in an issue, pull request, or commit. -* `target_url_template` - (Required) The template of the target URL used for the links; must be a valid URL and contain `` for the reference number +- `target_url_template` - (Required) The template of the target URL used for the links; must be a valid URL and contain `` for the reference number -* `is_alphanumeric` - (Optional) Whether this autolink reference matches alphanumeric characters. If false, this autolink reference only matches numeric characters. Default is true. +- `is_alphanumeric` - (Optional) Whether this autolink reference matches alphanumeric characters. If false, this autolink reference only matches numeric characters. Default is true. ## Attributes Reference The following additional attributes are exported: -* `etag` - An etag representing the autolink reference object. +- `etag` - An etag representing the autolink reference object. +- `repository_id` - The ID of the repository. ## Import