Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -211,6 +211,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_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
Expand Down
218 changes: 218 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,218 @@
package github

import (
"context"
"log"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/shurcooL/githubv4"
)

func resourceGithubEnterpriseIpAllowListEntry() *schema.Resource {
return &schema.Resource{
Create: resourceGithubEnterpriseIpAllowListEntryCreate,
Read: resourceGithubEnterpriseIpAllowListEntryRead,
Update: resourceGithubEnterpriseIpAllowListEntryUpdate,
Delete: resourceGithubEnterpriseIpAllowListEntryDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
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 a top-level Description field

"enterprise_slug": {
Type: schema.TypeString,
Required: true,
Description: "The slug of the enterprise to apply the IP allow list entry to.",
},
"ip": {
Type: schema.TypeString,
Required: 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.",
},
},
}
}

func resourceGithubEnterpriseIpAllowListEntryCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v4client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

// 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 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 err
}

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

return resourceGithubEnterpriseIpAllowListEntryRead(d, meta)
}

func resourceGithubEnterpriseIpAllowListEntryRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v4client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

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]interface{}{
"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") {
log.Printf("[INFO] Removing IP allow list entry (%s) from state because it no longer exists in GitHub", d.Id())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use tflog.Info instead of log.Printf

d.SetId("")
return nil
}
return err
}

entry := query.Node.IpAllowListEntry

d.Set("ip", entry.AllowListValue)
d.Set("name", entry.Name)
d.Set("is_active", entry.IsActive)
d.Set("created_at", entry.CreatedAt)
d.Set("updated_at", entry.UpdatedAt)
d.Set("enterprise_slug", entry.Owner.Enterprise.Slug)

return nil
}

func resourceGithubEnterpriseIpAllowListEntryUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v4client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

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 err
}

return resourceGithubEnterpriseIpAllowListEntryRead(d, meta)
}

func resourceGithubEnterpriseIpAllowListEntryDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v4client
ctx := context.WithValue(context.Background(), ctxId, d.Id())

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)
if err != nil {
return err
}

d.SetId("")
return nil
}

// Helper function to get Enterprise ID from slug
func getEnterpriseID(ctx context.Context, client *githubv4.Client, enterpriseSlug string) (string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAIK enterprise slug is the ID, the GraphQL ID shouldn't pollute TF.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If i understand this correctly, you'd still want the resource to reference the slug (and not the GraphQL ID) like so:

resource "github_enterprise_ip_allow_list_entry" "example" {
  enterprise_slug = "my-enterprise-slug"
  ip              = "1.2.3.4"
}

If you pass enterprise_slug to the CreateIpAllowListEntry mutation it will fail with Error: Could not resolve to a node with the global id of 'my-enterprise-slug' (i tested with a real one). It looks to need the GraphQL ID, and not the slug. If we keep the lookup like this, we should be able to keep the GraphQL ID out of the terraform and have the lookup take place behind the scenes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/input-objects#createipallowlistentryinput

I think it makes sense that it's not the enterprise_slug as AFAIK that Mutation also works for Organizations, which could have the same slug as the enterprise

Copy link
Collaborator

Choose a reason for hiding this comment

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

The GraphQL ID may be required, but it shouldn't be the resource ID as that should be the slug. I'm not fully familiar with the GitHub GraphQL implementation but isn't the whole point of GraphQL that you can do everything with a single query; in which case why can't you use the slug to lookup the GraphQL ID in the same query?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, you can combine multiple "requests" in a single GQL query, but directly dependant things aren't possible AFAIK.
But there one can then just use sequential queries. And one can't mix mutations and queries in a single request

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can't see a pending REST implementation. We may have to leak a GraphQL ID into the state after all. I'd suggest using <graphql-id>:<name> for the ID so if we get a REST implementation we can remove the GraphQL dependency.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't recommend using name as it's actually a description field and can contain long text.

And since name is mutable, it would change the ID even if the actual resource didn't change.

Existing IP entry in UI: image
Add entry in UI:
image
Edit entry in UI:
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we sure IP isn't unique in the context of an enterprise?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, we have had multiple duplicate IPs. We could treat it as unique even if the API allows duplicate IPS. One reason for allowing duplicate IPs would be ease of management.

For example the IPs in api.github.com/meta have duplicates between products. If I'd be syncing these blindly to the allowlist with the product name (which we are doing) then it needs to enable duplicate IPS 😬

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not that it's relevant to this PR, but wouldn't you run them through distinct by default?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This function seems to exist already in resource_github_enterprise_organization.go, could you re-use that?

Maybe it could be moved to util_v4.go?

var query struct {
Enterprise struct {
ID githubv4.ID
} `graphql:"enterprise(slug: $slug)"`
}

variables := map[string]interface{}{
"slug": githubv4.String(enterpriseSlug),
}

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

return query.Enterprise.ID.(string), nil
}
101 changes: 101 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,101 @@
package github

import (
"fmt"
"testing"

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

func TestAccGithubEnterpriseIpAllowListEntry_basic(t *testing.T) {
t.Skip("Acceptance test requires a real GitHub Enterprise environment")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
t.Skip("Acceptance test requires a real GitHub Enterprise environment")


resourceName := "github_enterprise_ip_allow_list_entry.test"
enterpriseSlug := "test-enterprise"
ip := "192.168.1.0/24"
name := "Test Entry"
isActive := true

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)

Check failure on line 21 in github/resource_github_enterprise_ip_allow_list_entry_test.go

View workflow job for this annotation

GitHub Actions / Continuous Integration

undefined: testAccPreCheck
testAccPreCheckEnterprise(t)
},
Providers: testAccProviders,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Providers: testAccProviders,
ProviderFactories: providerFactories,

Steps: []resource.TestStep{
{
Config: testAccGithubEnterpriseIpAllowListEntryConfig(enterpriseSlug, ip, name, isActive),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "enterprise_slug", enterpriseSlug),
resource.TestCheckResourceAttr(resourceName, "ip", ip),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "is_active", fmt.Sprintf("%t", isActive)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
resource.TestCheckResourceAttr(resourceName, "is_active", fmt.Sprintf("%t", isActive)),
resource.TestCheckResourceAttr(resourceName, "is_active", strconv.FormatBool(isActive)),

),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccGithubEnterpriseIpAllowListEntry_update(t *testing.T) {
t.Skip("Acceptance test requires a real GitHub Enterprise environment")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
t.Skip("Acceptance test requires a real GitHub Enterprise environment")


resourceName := "github_enterprise_ip_allow_list_entry.test"
enterpriseSlug := "test-enterprise"
ip := "192.168.1.0/24"
name := "Test Entry"
isActive := true

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

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)

Check failure on line 59 in github/resource_github_enterprise_ip_allow_list_entry_test.go

View workflow job for this annotation

GitHub Actions / Continuous Integration

undefined: testAccPreCheck (typecheck)
testAccPreCheckEnterprise(t)
},
Providers: testAccProviders,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Providers: testAccProviders,
ProviderFactories: providerFactories,

Steps: []resource.TestStep{
{
Config: testAccGithubEnterpriseIpAllowListEntryConfig(enterpriseSlug, ip, name, isActive),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "enterprise_slug", enterpriseSlug),
resource.TestCheckResourceAttr(resourceName, "ip", ip),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "is_active", fmt.Sprintf("%t", isActive)),
),
},
{
Config: testAccGithubEnterpriseIpAllowListEntryConfig(enterpriseSlug, updatedIP, updatedName, updatedIsActive),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "enterprise_slug", enterpriseSlug),
resource.TestCheckResourceAttr(resourceName, "ip", updatedIP),
resource.TestCheckResourceAttr(resourceName, "name", updatedName),
resource.TestCheckResourceAttr(resourceName, "is_active", fmt.Sprintf("%t", updatedIsActive)),
),
},
},
})
}

func testAccGithubEnterpriseIpAllowListEntryConfig(enterpriseSlug, ip, name string, isActive bool) string {
return fmt.Sprintf(`
resource "github_enterprise_ip_allow_list_entry" "test" {
enterprise_slug = "%s"
ip = "%s"
name = "%s"
is_active = %t
}
`, enterpriseSlug, ip, name, isActive)
}

func testAccPreCheckEnterprise(t *testing.T) {
if v := testAccProvider.Meta().(*Owner).name; v == "" {
t.Fatal("The GITHUB_ENTERPRISE_SLUG environment variable must be set for enterprise tests")
}
}
38 changes: 38 additions & 0 deletions website/docs/r/enterprise_ip_allow_list_entry.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
layout: "github"
page_title: "GitHub: github_enterprise_ip_allow_list_entry"
description: |-
Creates and manages IP allow list entries within a GitHub Enterprise
---

# github_enterprise_ip_allow_list_entry

This resource allows you to create and manage IP allow list entries for a GitHub Enterprise account. IP allow list entries define IP addresses or ranges that are permitted to access private resources in the enterprise.

## Example Usage

```hcl
resource "github_enterprise_ip_allow_list_entry" "test" {
enterprise_slug = "my-enterprise"
ip = "192.168.1.0/20"
name = "My IP Range Name"
is_active = true
}
```

## Argument Reference

The following arguments are supported:

* `enterprise_slug` - (Required) The slug of the enterprise.
* `ip` - (Required) An IP address or range of IP addresses in CIDR notation.
* `name` - (Optional) A descriptive name for the IP allow list entry.
* `is_active` - (Optional) Whether the entry is currently active. Default: true.

## Import

This resource can be imported using the ID of the IP allow list entry:

```bash
$ terraform import github_enterprise_ip_allow_list_entry.test IALE_kwHOC1234567890a
```
Loading