diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..a2829596c7 100644 --- a/github/provider.go +++ b/github/provider.go @@ -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(), diff --git a/github/resource_github_enterprise_ip_allow_list_entry.go b/github/resource_github_enterprise_ip_allow_list_entry.go new file mode 100644 index 0000000000..11e6759d92 --- /dev/null +++ b/github/resource_github_enterprise_ip_allow_list_entry.go @@ -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) + } + + 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 + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_enterprise_ip_allow_list_entry_test.go b/github/resource_github_enterprise_ip_allow_list_entry_test.go new file mode 100644 index 0000000000..ba7f63467a --- /dev/null +++ b/github/resource_github_enterprise_ip_allow_list_entry_test.go @@ -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)), + ), + }, + }, + }) + }) +} diff --git a/github/resource_github_enterprise_organization.go b/github/resource_github_enterprise_organization.go index acc706a53a..d2dd093cfd 100644 --- a/github/resource_github_enterprise_organization.go +++ b/github/resource_github_enterprise_organization.go @@ -268,7 +268,7 @@ func resourceGithubEnterpriseOrganizationImport(data *schema.ResourceData, meta v4 := meta.(*Owner).v4client ctx := context.Background() - enterpriseId, err := getEnterpriseId(ctx, v4, parts[0]) + enterpriseId, err := getEnterpriseID(ctx, v4, parts[0]) if err != nil { return nil, err } @@ -287,20 +287,6 @@ func resourceGithubEnterpriseOrganizationImport(data *schema.ResourceData, meta return []*schema.ResourceData{data}, nil } -func getEnterpriseId(ctx context.Context, v4 *githubv4.Client, enterpriseSlug string) (string, error) { - var query struct { - Enterprise struct { - ID githubv4.String - } `graphql:"enterprise(slug: $enterpriseSlug)"` - } - - err := v4.Query(ctx, &query, map[string]any{"enterpriseSlug": githubv4.String(enterpriseSlug)}) - if err != nil { - return "", err - } - return string(query.Enterprise.ID), nil -} - func getOrganizationId(ctx context.Context, v4 *githubv4.Client, orgName string) (string, error) { var query struct { Organization struct { diff --git a/github/util_v4.go b/github/util_v4.go index e0f9ac1df5..93654a6e74 100644 --- a/github/util_v4.go +++ b/github/util_v4.go @@ -1,6 +1,8 @@ package github import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/shurcooL/githubv4" ) @@ -50,3 +52,22 @@ func githubv4IDSliceEmpty(ss []string) []githubv4.ID { func githubv4NewStringSlice(v []githubv4.String) *[]githubv4.String { return &v } func githubv4NewIDSlice(v []githubv4.ID) *[]githubv4.ID { return &v } + +func getEnterpriseID(ctx context.Context, client *githubv4.Client, enterpriseSlug string) (string, error) { + var query struct { + Enterprise struct { + ID githubv4.ID + } `graphql:"enterprise(slug: $slug)"` + } + + variables := map[string]any{ + "slug": githubv4.String(enterpriseSlug), + } + + err := client.Query(ctx, &query, variables) + if err != nil { + return "", err + } + + return query.Enterprise.ID.(string), nil +} diff --git a/website/docs/r/enterprise_ip_allow_list_entry.html.markdown b/website/docs/r/enterprise_ip_allow_list_entry.html.markdown new file mode 100644 index 0000000000..45817dffba --- /dev/null +++ b/website/docs/r/enterprise_ip_allow_list_entry.html.markdown @@ -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 +``` \ No newline at end of file