Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0aca2ad
feat(utils): add shared helper functions for cost centers
github-actions[bot] Feb 4, 2026
018a7fa
feat(cost-centers): add retry logic utilities for cost center operations
github-actions[bot] Feb 4, 2026
8d9afc8
feat(cost-centers): add github_enterprise_cost_center resource
github-actions[bot] Feb 4, 2026
4003e75
feat(cost-centers): add github_enterprise_cost_center_users resource
github-actions[bot] Feb 4, 2026
c29fa05
feat(cost-centers): add github_enterprise_cost_center_organizations r…
github-actions[bot] Feb 4, 2026
141c76d
feat(cost-centers): add github_enterprise_cost_center_repositories re…
github-actions[bot] Feb 4, 2026
0a2c615
feat(cost-centers): add data sources for enterprise cost centers
github-actions[bot] Feb 4, 2026
7ac14e9
feat(cost-centers): register cost center resources and data sources
github-actions[bot] Feb 4, 2026
2fc331b
docs(cost-centers): add usage example
github-actions[bot] Feb 4, 2026
5d4ddca
fix(cost-centers): update go-github import to v82
github-actions[bot] Feb 4, 2026
683840a
fix: removed linter config, it shouldn't be needed
vmvarela Feb 9, 2026
eb10c03
fix(cost-centers): remove unnecessary expandStringSet function
vmvarela Feb 9, 2026
c36c49a
fix(cost-centers): check deleted state in Read instead of Update
vmvarela Feb 9, 2026
44d06ac
fix(cost-centers): separate Create and Update for users resource
vmvarela Feb 9, 2026
8fa9583
fix(cost-centers): separate Create and Update for organizations resource
vmvarela Feb 9, 2026
5240f29
fix(cost-centers): separate Create and Update for repositories resource
vmvarela Feb 9, 2026
eaabdc1
fix(cost-centers): correct resource type strings in CheckDestroy func…
vmvarela Feb 9, 2026
4696150
refactor(cost-centers): use type constants instead of magic strings
vmvarela Feb 9, 2026
1855b1c
fix(cost-centers): use terraform-plugin-testing imports in test files
vmvarela Feb 14, 2026
aa2ad07
fix(cost-centers): document generic chunk helper lint exception
vmvarela Feb 17, 2026
847022d
fix: address review - rename single-char variables and remove unneces…
vmvarela Feb 18, 2026
12a759f
fix: address review - rename maxResourcesPerRequest to maxCostCenterR…
vmvarela Feb 18, 2026
f10a9e2
fix: address review - simplify import test with ImportStateIdPrefix
vmvarela Feb 18, 2026
538b6e6
fix: address review - add unit tests for errIs404, errIsRetryable, ch…
vmvarela Feb 18, 2026
ce07138
fix: address review - migrate cost center tests to ConfigStateChecks
vmvarela Feb 18, 2026
4ad701c
fix(cost-centers): update go-github import from v82 to v83
vmvarela Feb 23, 2026
a567202
fix(cost-centers): add default 'all' value to state field in cost cen…
vmvarela Feb 23, 2026
e6d0946
fix(cost-center): populate name on import via API lookup
vmvarela Feb 24, 2026
d09817c
fix(cost-center): align sub-resource IDs with main resource
vmvarela Feb 24, 2026
a2113ff
fix(cost-center): validate no existing assignments on Create
vmvarela Feb 24, 2026
98fe0fb
refactor(cost-center): simplify Update diff with map pattern
vmvarela Feb 24, 2026
d2cb565
fix(cost-center): Delete removes all linked resources from API
vmvarela Feb 24, 2026
7c39d61
fix(cost-center): add missing sub-resources to website sidebar
vmvarela Feb 24, 2026
dd6d4bb
fix: update go-github import to v84 and remove duplicate team helpers…
vmvarela Mar 18, 2026
4ae98a1
fix(cost-centers): use third-person verb in data source descriptions
vmvarela Mar 23, 2026
f9c34a9
chore(lint): exclude modernize newexpr rule for Ptr calls
vmvarela Mar 23, 2026
cf198b9
fix(cost-centers): remove defensive nil check and use deleteResourceO…
vmvarela Mar 23, 2026
ca39dac
fix: undo .golangci.yml changes
vmvarela Mar 25, 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
115 changes: 115 additions & 0 deletions examples/cost_centers/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
terraform {
required_providers {
github = {
source = "integrations/github"
version = "~> 6.11"
}
}
}

provider "github" {
token = var.github_token
owner = var.enterprise_slug
}

variable "github_token" {
description = "GitHub classic personal access token (PAT) for an enterprise admin"
type = string
sensitive = true
}

variable "enterprise_slug" {
description = "The GitHub Enterprise slug"
type = string
}

variable "cost_center_name" {
description = "Name for the cost center"
type = string
}

variable "users" {
description = "Usernames to assign to the cost center"
type = list(string)
default = []
}

variable "organizations" {
description = "Organization logins to assign to the cost center"
type = list(string)
default = []
}

variable "repositories" {
description = "Repositories (full name, e.g. org/repo) to assign to the cost center"
type = list(string)
default = []
}

# The cost center resource manages only the cost center entity itself.
resource "github_enterprise_cost_center" "example" {
enterprise_slug = var.enterprise_slug
name = var.cost_center_name
}

# Use separate authoritative resources for assignments.
# These are optional - only create them if you have items to assign.

resource "github_enterprise_cost_center_users" "example" {
count = length(var.users) > 0 ? 1 : 0

enterprise_slug = var.enterprise_slug
cost_center_id = github_enterprise_cost_center.example.id
usernames = var.users
}

resource "github_enterprise_cost_center_organizations" "example" {
count = length(var.organizations) > 0 ? 1 : 0

enterprise_slug = var.enterprise_slug
cost_center_id = github_enterprise_cost_center.example.id
organization_logins = var.organizations
}

resource "github_enterprise_cost_center_repositories" "example" {
count = length(var.repositories) > 0 ? 1 : 0

enterprise_slug = var.enterprise_slug
cost_center_id = github_enterprise_cost_center.example.id
repository_names = var.repositories
}

# Data sources for reading cost center information
data "github_enterprise_cost_center" "by_id" {
enterprise_slug = var.enterprise_slug
cost_center_id = github_enterprise_cost_center.example.id
}

data "github_enterprise_cost_centers" "active" {
enterprise_slug = var.enterprise_slug
state = "active"

depends_on = [github_enterprise_cost_center.example]
}

output "cost_center" {
description = "Created cost center"
value = {
id = github_enterprise_cost_center.example.id
name = github_enterprise_cost_center.example.name
state = github_enterprise_cost_center.example.state
azure_subscription = github_enterprise_cost_center.example.azure_subscription
}
}

output "cost_center_from_data_source" {
description = "Cost center fetched by data source (includes all assignments)"
value = {
id = data.github_enterprise_cost_center.by_id.cost_center_id
name = data.github_enterprise_cost_center.by_id.name
state = data.github_enterprise_cost_center.by_id.state
users = sort(tolist(data.github_enterprise_cost_center.by_id.users))
organizations = sort(tolist(data.github_enterprise_cost_center.by_id.organizations))
repositories = sort(tolist(data.github_enterprise_cost_center.by_id.repositories))
}
}
113 changes: 113 additions & 0 deletions github/data_source_github_enterprise_cost_center.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package github

import (
"context"

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

func dataSourceGithubEnterpriseCostCenter() *schema.Resource {
return &schema.Resource{
Description: "Retrieves information about a specific GitHub enterprise cost center.",
ReadContext: dataSourceGithubEnterpriseCostCenterRead,

Schema: map[string]*schema.Schema{
"enterprise_slug": {
Type: schema.TypeString,
Required: true,
Description: "The slug of the enterprise.",
},
"cost_center_id": {
Type: schema.TypeString,
Required: true,
Description: "The ID of the cost center.",
},
"name": {
Type: schema.TypeString,
Computed: true,
Description: "The name of the cost center.",
},
"state": {
Type: schema.TypeString,
Computed: true,
Description: "The state of the cost center.",
},
"azure_subscription": {
Type: schema.TypeString,
Computed: true,
Description: "The Azure subscription associated with the cost center.",
},
"users": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "The usernames assigned to this cost center.",
},
"organizations": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "The organization logins assigned to this cost center.",
},
"repositories": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "The repositories (full name) assigned to this cost center.",
},
},
}
}

func dataSourceGithubEnterpriseCostCenterRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v3client
enterpriseSlug := d.Get("enterprise_slug").(string)
costCenterID := d.Get("cost_center_id").(string)

cc, _, err := client.Enterprise.GetCostCenter(ctx, enterpriseSlug, costCenterID)
if err != nil {
return diag.FromErr(err)
}

d.SetId(costCenterID)
if err := d.Set("name", cc.Name); err != nil {
return diag.FromErr(err)
}

if err := d.Set("state", cc.GetState()); err != nil {
return diag.FromErr(err)
}
if err := d.Set("azure_subscription", cc.GetAzureSubscription()); err != nil {
return diag.FromErr(err)
}

users := make([]string, 0)
organizations := make([]string, 0)
repositories := make([]string, 0)
for _, resource := range cc.Resources {
if resource == nil {
continue
}
switch resource.Type {
case CostCenterResourceTypeUser:
users = append(users, resource.Name)
case CostCenterResourceTypeOrg:
organizations = append(organizations, resource.Name)
case CostCenterResourceTypeRepo:
repositories = append(repositories, resource.Name)
}
}

if err := d.Set("users", users); err != nil {
return diag.FromErr(err)
}
if err := d.Set("organizations", organizations); err != nil {
return diag.FromErr(err)
}
if err := d.Set("repositories", repositories); err != nil {
return diag.FromErr(err)
}

return nil
}
44 changes: 44 additions & 0 deletions github/data_source_github_enterprise_cost_center_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package github

import (
"fmt"
"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/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

func TestAccGithubEnterpriseCostCenterDataSource(t *testing.T) {
randomID := acctest.RandString(5)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessEnterprise(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{{
Config: fmt.Sprintf(`
data "github_enterprise" "enterprise" {
slug = "%s"
}

resource "github_enterprise_cost_center" "test" {
enterprise_slug = data.github_enterprise.enterprise.slug
name = "%s%s"
}

data "github_enterprise_cost_center" "test" {
enterprise_slug = data.github_enterprise.enterprise.slug
cost_center_id = github_enterprise_cost_center.test.id
}
`, testAccConf.enterpriseSlug, testResourcePrefix, randomID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.CompareValuePairs("data.github_enterprise_cost_center.test", tfjsonpath.New("cost_center_id"), "github_enterprise_cost_center.test", tfjsonpath.New("id"), compare.ValuesSame()),
statecheck.CompareValuePairs("data.github_enterprise_cost_center.test", tfjsonpath.New("name"), "github_enterprise_cost_center.test", tfjsonpath.New("name"), compare.ValuesSame()),
statecheck.ExpectKnownValue("data.github_enterprise_cost_center.test", tfjsonpath.New("state"), knownvalue.StringExact("active")),
},
}},
})
}
100 changes: 100 additions & 0 deletions github/data_source_github_enterprise_cost_centers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package github

import (
"context"

"github.com/google/go-github/v84/github"
"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 dataSourceGithubEnterpriseCostCenters() *schema.Resource {
return &schema.Resource{
Description: "Retrieves a list of GitHub enterprise cost centers.",
ReadContext: dataSourceGithubEnterpriseCostCentersRead,

Schema: map[string]*schema.Schema{
"enterprise_slug": {
Type: schema.TypeString,
Required: true,
Description: "The slug of the enterprise.",
},
"state": {
Type: schema.TypeString,
Optional: true,
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
Optional: true,
Optional: true,
Default: "all",

If you add a default your logic in the read function would be much simpler.

Copy link
Author

Choose a reason for hiding this comment

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

Done

Default: "all",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "active", "deleted"}, false)),
Description: "Filter cost centers by state. Valid values are 'all', 'active', and 'deleted'.",
},
"cost_centers": {
Type: schema.TypeSet,
Computed: true,
Description: "The list of cost centers.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: "The cost center ID.",
},
"name": {
Type: schema.TypeString,
Computed: true,
Description: "The name of the cost center.",
},
"state": {
Type: schema.TypeString,
Computed: true,
Description: "The state of the cost center.",
},
"azure_subscription": {
Type: schema.TypeString,
Computed: true,
Description: "The Azure subscription associated with the cost center.",
},
},
},
},
},
}
}

func dataSourceGithubEnterpriseCostCentersRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*Owner).v3client
enterpriseSlug := d.Get("enterprise_slug").(string)
stateFilter := d.Get("state").(string)

var opts github.ListCostCenterOptions
if stateFilter != "all" {
opts.State = github.Ptr(stateFilter)
}

result, _, err := client.Enterprise.ListCostCenters(ctx, enterpriseSlug, &opts)
if err != nil {
return diag.FromErr(err)
}

items := make([]any, 0, len(result.CostCenters))
for _, cc := range result.CostCenters {
if cc == nil {
continue
}
items = append(items, map[string]any{
"id": cc.ID,
"name": cc.Name,
"state": cc.GetState(),
"azure_subscription": cc.GetAzureSubscription(),
})
}

id, err := buildID(enterpriseSlug, stateFilter)
if err != nil {
return diag.FromErr(err)
}
d.SetId(id)
if err := d.Set("cost_centers", items); err != nil {
return diag.FromErr(err)
}
return nil
}
Loading