From 86890bf199637e4fbe8b965db314b9d12e9f694e Mon Sep 17 00:00:00 2001 From: Fulvio de Freitas Date: Tue, 12 May 2026 12:39:29 -0700 Subject: [PATCH 1/2] feat: read timeout for github_ip_ranges data source Apply a default 5m read timeout and use the Terraform read context for the metadata API call so the timeout is enforced. Reintroduces the intent of the prior stale PR for configurable read timeouts. Refs: #1961 #1978 --- docs/data-sources/ip_ranges.md | 6 ++++++ github/data_source_github_ip_ranges.go | 6 +++++- github/data_source_github_ip_ranges_test.go | 16 ++++++++++++++++ templates/data-sources/ip_ranges.md.tmpl | 6 ++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/data-sources/ip_ranges.md b/docs/data-sources/ip_ranges.md index caeb7a51b2..2a43b03998 100644 --- a/docs/data-sources/ip_ranges.md +++ b/docs/data-sources/ip_ranges.md @@ -14,6 +14,12 @@ Use this data source to retrieve information about GitHub's IP addresses. data "github_ip_ranges" "test" {} ``` +## Timeouts + +The `timeouts` block allows you to configure [timeouts](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when reading the GitHub IP ranges from the metadata API. + ## Attributes Reference - `actions` - An array of IP addresses in CIDR format specifying the addresses that incoming requests from GitHub Actions will originate from. diff --git a/github/data_source_github_ip_ranges.go b/github/data_source_github_ip_ranges.go index f92e687542..87b2ef977d 100644 --- a/github/data_source_github_ip_ranges.go +++ b/github/data_source_github_ip_ranges.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -13,6 +14,9 @@ func dataSourceGithubIpRanges() *schema.Resource { return &schema.Resource{ Description: "Get the GitHub IP ranges used by various GitHub services.", ReadContext: dataSourceGithubIpRangesRead, + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, Schema: map[string]*schema.Schema{ "hooks": { Type: schema.TypeList, @@ -198,7 +202,7 @@ func dataSourceGithubIpRanges() *schema.Resource { func dataSourceGithubIpRangesRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { owner := meta.(*Owner) - api, _, err := owner.v3client.Meta.Get(owner.StopContext) + api, _, err := owner.v3client.Meta.Get(ctx) if err != nil { return diag.FromErr(err) } diff --git a/github/data_source_github_ip_ranges_test.go b/github/data_source_github_ip_ranges_test.go index 74eaf62d2f..713ce1b9fb 100644 --- a/github/data_source_github_ip_ranges_test.go +++ b/github/data_source_github_ip_ranges_test.go @@ -2,6 +2,7 @@ package github import ( "testing" + "time" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" @@ -9,6 +10,21 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) +func TestUnitGithubIpRangesDataSource_schemaTimeouts(t *testing.T) { + t.Parallel() + + r := dataSourceGithubIpRanges() + if r.Timeouts == nil { + t.Fatal("expected Timeouts to be configured on github_ip_ranges data source") + } + if r.Timeouts.Read == nil { + t.Fatal("expected default read timeout to be configured") + } + if got := *r.Timeouts.Read; got != 5*time.Minute { + t.Fatalf("expected default read timeout 5m, got %v", got) + } +} + func TestAccGithubIpRangesDataSource(t *testing.T) { t.Run("reads IP ranges without error", func(t *testing.T) { config := `data "github_ip_ranges" "test" {}` diff --git a/templates/data-sources/ip_ranges.md.tmpl b/templates/data-sources/ip_ranges.md.tmpl index 9d19c4dcb3..69769ad06c 100644 --- a/templates/data-sources/ip_ranges.md.tmpl +++ b/templates/data-sources/ip_ranges.md.tmpl @@ -12,6 +12,12 @@ Use this data source to retrieve information about GitHub's IP addresses. {{ tffile "examples/data-sources/ip_ranges/example_1.tf" }} +## Timeouts + +The `timeouts` block allows you to configure [timeouts](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when reading the GitHub IP ranges from the metadata API. + ## Attributes Reference - `actions` - An array of IP addresses in CIDR format specifying the addresses that incoming requests from GitHub Actions will originate from. From c716412566ecb341f4d7352d7f1e18adc2a9f946 Mon Sep 17 00:00:00 2001 From: Fulvio de Freitas Date: Fri, 14 Aug 2026 12:38:30 -0700 Subject: [PATCH 2/2] test: verify github_ip_ranges read honors the caller deadline Replace the schema introspection assertions with tests that exercise the read path: a stalled metadata endpoint must abort at the deadline instead of waiting for the response, and a normal response must still populate the IP ranges. Also cover a configured timeouts block in the acceptance test. --- github/data_source_github_ip_ranges_test.go | 106 ++++++++++++++++++-- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/github/data_source_github_ip_ranges_test.go b/github/data_source_github_ip_ranges_test.go index ec2b4036cc..0b069a455a 100644 --- a/github/data_source_github_ip_ranges_test.go +++ b/github/data_source_github_ip_ranges_test.go @@ -1,33 +1,117 @@ package github import ( + "context" + "net/http" + "net/http/httptest" "testing" "time" + "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) -func TestUnitGithubIpRangesDataSource_schemaTimeouts(t *testing.T) { +func TestGithubIpRangesDataSourceRead(t *testing.T) { t.Parallel() - r := dataSourceGithubIpRanges() - if r.Timeouts == nil { - t.Fatal("expected Timeouts to be configured on github_ip_ranges data source") - } - if r.Timeouts.Read == nil { - t.Fatal("expected default read timeout to be configured") - } - if got := *r.Timeouts.Read; got != 5*time.Minute { - t.Fatalf("expected default read timeout 5m, got %v", got) - } + t.Run("gives up on a stalled metadata request once the deadline expires", func(t *testing.T) { + t.Parallel() + + serverDelay := 5 * time.Second + readDeadline := 200 * time.Millisecond + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + case <-time.After(serverDelay): + mustWrite(w, `{}`) + } + })) + defer ts.Close() + + // StopContext mirrors the provider configuration and never expires, so the + // read has to use the context it is called with to respect the deadline. + meta := &Owner{ + v3client: mustCreateTestGitHubClient(t, ts.URL), + StopContext: context.Background(), + } + + ctx, cancel := context.WithTimeout(t.Context(), readDeadline) + defer cancel() + + start := time.Now() + diags := dataSourceGithubIpRangesRead(ctx, dataSourceGithubIpRanges().TestResourceData(), meta) + elapsed := time.Since(start) + + if !diags.HasError() { + t.Fatal("expected an error when the read deadline expires before the response arrives") + } + if elapsed >= serverDelay { + t.Fatalf("read waited %s for the metadata response instead of honoring the %s deadline", elapsed, readDeadline) + } + }) + + t.Run("populates IP ranges when the response arrives before the deadline", func(t *testing.T) { + t.Parallel() + + ts := githubApiMock([]*mockResponse{ + mustGetTestMockResponse(t, "/meta", http.StatusOK, &github.APIMeta{ + Hooks: []string{"192.0.2.0/24", "2001:db8::/32"}, + }), + }) + defer ts.Close() + + meta := &Owner{v3client: mustCreateTestGitHubClient(t, ts.URL)} + + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + + d := dataSourceGithubIpRanges().TestResourceData() + diags := dataSourceGithubIpRangesRead(ctx, d, meta) + if diags.HasError() { + t.Fatalf("unexpected error: %v", diags) + } + + if got, want := d.Get("hooks_ipv4").([]any), "192.0.2.0/24"; len(got) != 1 || got[0] != want { + t.Errorf("expected hooks_ipv4 to be [%s], got %v", want, got) + } + if got, want := d.Get("hooks_ipv6").([]any), "2001:db8::/32"; len(got) != 1 || got[0] != want { + t.Errorf("expected hooks_ipv6 to be [%s], got %v", want, got) + } + }) } func TestAccGithubIpRangesDataSource(t *testing.T) { t.Parallel() + t.Run("reads IP ranges with a configured read timeout", func(t *testing.T) { + t.Parallel() + + config := ` + data "github_ip_ranges" "test" { + timeouts { + read = "2m" + } + } + ` + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_ip_ranges.test", tfjsonpath.New("actions_ipv4"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("data.github_ip_ranges.test", tfjsonpath.New("actions_ipv6"), knownvalue.NotNull()), + }, + }, + }, + }) + }) + t.Run("reads IP ranges without error", func(t *testing.T) { t.Parallel()