Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
943df18
Adding github_enterprise_ip_allow_list_entry resource
ErikElkins May 2, 2025
eb02154
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Jul 11, 2025
935e689
Merge branch 'main' into feat/enterprise-ip-allow-list
nickfloyd Jan 13, 2026
83a0055
Update github/resource_github_enterprise_ip_allow_list_entry.go
ErikElkins Jan 13, 2026
c3735ac
Update github/resource_github_enterprise_ip_allow_list_entry.go
ErikElkins Jan 13, 2026
e1b06a2
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Jan 13, 2026
833f3f1
Update github/resource_github_enterprise_ip_allow_list_entry.go
ErikElkins Jan 13, 2026
60c686b
Update github/resource_github_enterprise_ip_allow_list_entry_test.go
ErikElkins Jan 13, 2026
f667dcc
Update github/resource_github_enterprise_ip_allow_list_entry_test.go
ErikElkins Jan 13, 2026
06b2e43
Code review fixes
ErikElkins Jan 13, 2026
f8d8807
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Jan 24, 2026
aed945b
Fixes from code review
ErikElkins Jan 24, 2026
a032d9d
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Feb 10, 2026
b076b2a
Fixing code review comments
ErikElkins Feb 10, 2026
d5a1915
Update resource_github_enterprise_ip_allow_list_entry.go
ErikElkins Feb 14, 2026
2511b0b
Update resource_github_enterprise_ip_allow_list_entry.go
ErikElkins Feb 14, 2026
4c9ac44
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Feb 20, 2026
d593d56
Update github/resource_github_enterprise_ip_allow_list_entry_test.go
ErikElkins Feb 20, 2026
10de6a7
Update github/resource_github_enterprise_ip_allow_list_entry.go
ErikElkins Feb 20, 2026
4474a4f
Code review changes
ErikElkins Feb 20, 2026
01d61fc
Fixing config in test
ErikElkins Feb 20, 2026
03d79ef
Flattening update test
ErikElkins Feb 20, 2026
4e50668
Adding error handling
ErikElkins Feb 20, 2026
252cfe4
Simplifying import function
ErikElkins Feb 20, 2026
0369b10
Fix docs
ErikElkins Feb 20, 2026
8918bb8
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Feb 20, 2026
2eb508d
Fixing code review changes
ErikElkins Feb 23, 2026
85b1956
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Feb 23, 2026
5dd8ef6
Fixing lint
ErikElkins Feb 23, 2026
08aa432
Merge branch 'main' into feat/enterprise-ip-allow-list
ErikElkins Feb 23, 2026
c5e2e12
Adding error handling for missing global ID
ErikElkins Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ func Provider() *schema.Provider {
"github_user_ssh_key": resourceGithubUserSshKey(),
"github_enterprise_organization": resourceGithubEnterpriseOrganization(),
"github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(),
"github_enterprise_ip_allow_list_entry": resourceGithubEnterpriseIpAllowListEntry(),
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
Expand Down
283 changes: 283 additions & 0 deletions github/resource_github_enterprise_ip_allow_list_entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package github

import (
"context"
"strings"

"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/shurcooL/githubv4"
)

func resourceGithubEnterpriseIpAllowListEntry() *schema.Resource {
return &schema.Resource{
Description: "Manage a GitHub Enterprise IP Allow List Entry.",
CreateContext: resourceGithubEnterpriseIpAllowListEntryCreate,
ReadContext: resourceGithubEnterpriseIpAllowListEntryRead,
UpdateContext: resourceGithubEnterpriseIpAllowListEntryUpdate,
DeleteContext: resourceGithubEnterpriseIpAllowListEntryDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceGithubEnterpriseIpAllowListEntryImport,
},

Schema: map[string]*schema.Schema{
"enterprise_slug": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The slug of the enterprise to apply the IP allow list entry to.",
},
"ip": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "An IP address or range of IP addresses in CIDR notation.",
},
"name": {
Type: schema.TypeString,
Optional: true,
Description: "An optional name for the IP allow list entry.",
},
"is_active": {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Whether the entry is currently active.",
},
"created_at": {
Type: schema.TypeString,
Computed: true,
Description: "Timestamp of when the entry was created.",
},
"updated_at": {
Type: schema.TypeString,
Computed: true,
Description: "Timestamp of when the entry was last updated.",
},
},
}
}

func resourceGithubEnterpriseIpAllowListEntryCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v4client

// First, get the enterprise ID as we need it for the mutation
enterpriseSlug := d.Get("enterprise_slug").(string)
enterpriseID, err := getEnterpriseID(ctx, client, enterpriseSlug)
if err != nil {
return diag.FromErr(err)
}

// Then create the IP allow list entry
var mutation struct {
CreateIpAllowListEntry struct {
IpAllowListEntry struct {
ID githubv4.String
AllowListValue githubv4.String
Name githubv4.String
IsActive githubv4.Boolean
CreatedAt githubv4.String
UpdatedAt githubv4.String
}
} `graphql:"createIpAllowListEntry(input: $input)"`
}

name := d.Get("name").(string)
input := githubv4.CreateIpAllowListEntryInput{
OwnerID: githubv4.ID(enterpriseID),
AllowListValue: githubv4.String(d.Get("ip").(string)),
IsActive: githubv4.Boolean(d.Get("is_active").(bool)),
}

if name != "" {
input.Name = githubv4.NewString(githubv4.String(name))
}

err = client.Mutate(ctx, &mutation, input, nil)
if err != nil {
return diag.FromErr(err)
}

d.SetId(string(mutation.CreateIpAllowListEntry.IpAllowListEntry.ID))

if err := d.Set("created_at", mutation.CreateIpAllowListEntry.IpAllowListEntry.CreatedAt); err != nil {
return diag.FromErr(err)
}
if err := d.Set("updated_at", mutation.CreateIpAllowListEntry.IpAllowListEntry.UpdatedAt); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubEnterpriseIpAllowListEntryRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v4client

var query struct {
Node struct {
IpAllowListEntry struct {
ID githubv4.String
AllowListValue githubv4.String
Name githubv4.String
IsActive githubv4.Boolean
CreatedAt githubv4.String
UpdatedAt githubv4.String
Owner struct {
Enterprise struct {
Slug githubv4.String
} `graphql:"... on Enterprise"`
}
} `graphql:"... on IpAllowListEntry"`
} `graphql:"node(id: $id)"`
}

variables := map[string]any{
"id": githubv4.ID(d.Id()),
}

err := client.Query(ctx, &query, variables)
if err != nil {
if strings.Contains(err.Error(), "Could not resolve to a node with the global id") {
tflog.Info(ctx, "Removing IP allow list entry from state because it no longer exists in GitHub", map[string]any{
"id": d.Id(),
})
d.SetId("")
return nil
}
return diag.FromErr(err)
}

entry := query.Node.IpAllowListEntry
if err := d.Set("name", entry.Name); err != nil {
return diag.FromErr(err)
}
if err := d.Set("ip", entry.AllowListValue); err != nil {
return diag.FromErr(err)
}
if err := d.Set("is_active", entry.IsActive); err != nil {
return diag.FromErr(err)
}
if err := d.Set("created_at", entry.CreatedAt); err != nil {
return diag.FromErr(err)
}
if err := d.Set("updated_at", entry.UpdatedAt); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubEnterpriseIpAllowListEntryUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v4client

var mutation struct {
UpdateIpAllowListEntry struct {
IpAllowListEntry struct {
ID githubv4.String
AllowListValue githubv4.String
Name githubv4.String
IsActive githubv4.Boolean
UpdatedAt githubv4.String
}
} `graphql:"updateIpAllowListEntry(input: $input)"`
}

name := d.Get("name").(string)
input := githubv4.UpdateIpAllowListEntryInput{
IPAllowListEntryID: githubv4.ID(d.Id()),
AllowListValue: githubv4.String(d.Get("ip").(string)),
IsActive: githubv4.Boolean(d.Get("is_active").(bool)),
}

if name != "" {
input.Name = githubv4.NewString(githubv4.String(name))
}

err := client.Mutate(ctx, &mutation, input, nil)
if err != nil {
return diag.FromErr(err)
}

if err := d.Set("updated_at", mutation.UpdateIpAllowListEntry.IpAllowListEntry.UpdatedAt); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubEnterpriseIpAllowListEntryDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v4client

var mutation struct {
DeleteIpAllowListEntry struct {
ClientMutationID githubv4.String
} `graphql:"deleteIpAllowListEntry(input: $input)"`
}

input := githubv4.DeleteIpAllowListEntryInput{
IPAllowListEntryID: githubv4.ID(d.Id()),
}

err := client.Mutate(ctx, &mutation, input, nil)
// GraphQL will return a 200 OK if it couldn't find the global ID
if err != nil && !strings.Contains(err.Error(), "Could not resolve to a node with the global id") {
return diag.FromErr(err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add error handling that a 404 doesn't unnecessarily create a Terraform error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added error handling and a comment. The GraphQL API (and github go client) means it'll look kinda janky. Let me know if you'd rather have me remove it.

}

return nil
}

func resourceGithubEnterpriseIpAllowListEntryImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
client := meta.(*Owner).v4client

var query struct {
Node struct {
IpAllowListEntry struct {
ID githubv4.String
AllowListValue githubv4.String
Name githubv4.String
IsActive githubv4.Boolean
CreatedAt githubv4.String
UpdatedAt githubv4.String
Owner struct {
Enterprise struct {
Slug githubv4.String
} `graphql:"... on Enterprise"`
}
} `graphql:"... on IpAllowListEntry"`
} `graphql:"node(id: $id)"`
}

variables := map[string]any{
"id": githubv4.ID(d.Id()),
}

err := client.Query(ctx, &query, variables)
if err != nil {
return nil, err
}

entry := query.Node.IpAllowListEntry

if err := d.Set("enterprise_slug", string(entry.Owner.Enterprise.Slug)); err != nil {
return nil, err
}
if err := d.Set("ip", string(entry.AllowListValue)); err != nil {
return nil, err
}
if err := d.Set("name", entry.Name); err != nil {
return nil, err
}
if err := d.Set("is_active", entry.IsActive); err != nil {
return nil, err
}
if err := d.Set("created_at", entry.CreatedAt); err != nil {
return nil, err
}
if err := d.Set("updated_at", entry.UpdatedAt); err != nil {
return nil, err
}
Comment on lines +234 to +280
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Am I seeing things or did you re-add the API query to the Import func?

The ID should be enough as the Read func is the next thing to be called with futher Terraform operations and it does the exact same thing as Import. It doesn't seem useful to have an API interaction here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked for this, the query is required otherwise it'll be recreated du to the IP being unset and the import is a complete waste of time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the recommended approach by @stevehipwell here: #2649 (comment)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevehipwell Oh, right.
What if we add the IP to the import ID to circumvent the need for a API call? It's already QUITE difficult to run Import as finding the GQL ID of the allowlist entry requires one to query the API manually first 😬

Actually, I wonder if there is a way to make Import more user friendly? 🤔

Copy link
Contributor Author

@ErikElkins ErikElkins Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to do the import based on ip, but IP is not unique in the IP allow list. You could have the same IP and description multiple times in the list for one enterprise. :(

EDIT: It's unique based on IP AND description


return []*schema.ResourceData{d}, nil
}
96 changes: 96 additions & 0 deletions github/resource_github_enterprise_ip_allow_list_entry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package github

import (
"fmt"
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccGithubEnterpriseIpAllowListEntry(t *testing.T) {
t.Run("basic", func(t *testing.T) {
resourceName := "github_enterprise_ip_allow_list_entry.test"
ip := "192.168.1.0/24"
name := "Test Entry"
isActive := true

config := `
resource "github_enterprise_ip_allow_list_entry" "test" {
enterprise_slug = "%s"
ip = "%s"
name = "%s"
is_active = %t
}
`

resource.Test(t, resource.TestCase{
PreCheck: func() {
skipUnlessEnterprise(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(config, testAccConf.enterpriseSlug, ip, name, isActive),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "enterprise_slug", testAccConf.enterpriseSlug),
resource.TestCheckResourceAttr(resourceName, "ip", ip),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "is_active", strconv.FormatBool(isActive)),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
})
t.Run("update", func(t *testing.T) {
resourceName := "github_enterprise_ip_allow_list_entry.test"
ip := "192.168.1.0/24"
name := "Test Entry"
isActive := true

updatedIP := "10.0.0.0/16"
updatedName := "Updated Entry"
updatedIsActive := false

config := `
resource "github_enterprise_ip_allow_list_entry" "test" {
enterprise_slug = "%s"
ip = "%s"
name = "%s"
is_active = %t
}
`

resource.Test(t, resource.TestCase{
PreCheck: func() {
skipUnlessEnterprise(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(config, testAccConf.enterpriseSlug, ip, name, isActive),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "enterprise_slug", testAccConf.enterpriseSlug),
resource.TestCheckResourceAttr(resourceName, "ip", ip),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "is_active", fmt.Sprintf("%t", isActive)),
),
},
{
Config: fmt.Sprintf(config, testAccConf.enterpriseSlug, updatedIP, updatedName, updatedIsActive),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "enterprise_slug", testAccConf.enterpriseSlug),
resource.TestCheckResourceAttr(resourceName, "ip", updatedIP),
resource.TestCheckResourceAttr(resourceName, "name", updatedName),
resource.TestCheckResourceAttr(resourceName, "is_active", fmt.Sprintf("%t", updatedIsActive)),
),
},
},
})
})
}
Loading