Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ade5ab5
feat(utils): add shared helper functions for cost centers
github-actions[bot] Feb 4, 2026
b1fb4e7
feat(cost-centers): add retry logic utilities for cost center operations
github-actions[bot] Feb 4, 2026
5531bfe
feat(cost-centers): add github_enterprise_cost_center resource
github-actions[bot] Feb 4, 2026
7413302
feat(cost-centers): add github_enterprise_cost_center_users resource
github-actions[bot] Feb 4, 2026
023ecf7
feat(cost-centers): add github_enterprise_cost_center_organizations r…
github-actions[bot] Feb 4, 2026
efcec0c
feat(cost-centers): add github_enterprise_cost_center_repositories re…
github-actions[bot] Feb 4, 2026
2b36273
feat(cost-centers): add data sources for enterprise cost centers
github-actions[bot] Feb 4, 2026
7f8950a
feat(cost-centers): register cost center resources and data sources
github-actions[bot] Feb 4, 2026
f153510
docs(cost-centers): add usage example
github-actions[bot] Feb 4, 2026
91c18dd
fix(cost-centers): update go-github import to v82
github-actions[bot] Feb 4, 2026
a77624e
fix: removed linter config, it shouldn't be needed
vmvarela Feb 9, 2026
977c197
fix(cost-centers): remove unnecessary expandStringSet function
vmvarela Feb 9, 2026
71498bc
fix(cost-centers): check deleted state in Read instead of Update
vmvarela Feb 9, 2026
14c9cc8
fix(cost-centers): separate Create and Update for users resource
vmvarela Feb 9, 2026
99ae8d3
fix(cost-centers): separate Create and Update for organizations resource
vmvarela Feb 9, 2026
90ee7cc
fix(cost-centers): separate Create and Update for repositories resource
vmvarela Feb 9, 2026
92e7f60
fix(cost-centers): correct resource type strings in CheckDestroy func…
vmvarela Feb 9, 2026
55f4cbf
refactor(cost-centers): use type constants instead of magic strings
vmvarela Feb 9, 2026
f1fa841
fix(cost-centers): use terraform-plugin-testing imports in test files
vmvarela Feb 14, 2026
e513cb3
fix(cost-centers): document generic chunk helper lint exception
vmvarela Feb 17, 2026
fa610ad
fix: address review - rename single-char variables and remove unneces…
vmvarela Feb 18, 2026
553d8ca
fix: address review - rename maxResourcesPerRequest to maxCostCenterR…
vmvarela Feb 18, 2026
c9e8c33
fix: address review - simplify import test with ImportStateIdPrefix
vmvarela Feb 18, 2026
5d51fb3
fix: address review - add unit tests for errIs404, errIsRetryable, ch…
vmvarela Feb 18, 2026
cbe311a
fix: address review - migrate cost center tests to ConfigStateChecks
vmvarela Feb 18, 2026
44e296e
fix(cost-centers): update go-github import from v82 to v83
vmvarela Feb 23, 2026
7b9eb6c
fix(cost-centers): add default 'all' value to state field in cost cen…
vmvarela Feb 23, 2026
757d0b2
fix(cost-center): populate name on import via API lookup
vmvarela Feb 24, 2026
3f852a8
fix(cost-center): align sub-resource IDs with main resource
vmvarela Feb 24, 2026
c6c7127
fix(cost-center): validate no existing assignments on Create
vmvarela Feb 24, 2026
c9ae2ef
refactor(cost-center): simplify Update diff with map pattern
vmvarela Feb 24, 2026
eeb8acc
fix(cost-center): Delete removes all linked resources from API
vmvarela Feb 24, 2026
ac65797
fix(cost-center): add missing sub-resources to website sidebar
vmvarela Feb 24, 2026
c17c220
fix: update go-github import to v84 and remove duplicate team helpers…
vmvarela Mar 18, 2026
a8faeb6
fix(cost-centers): use third-person verb in data source descriptions
vmvarela Mar 23, 2026
18f9d54
chore(lint): exclude modernize newexpr rule for Ptr calls
vmvarela Mar 23, 2026
a88420b
fix(cost-centers): remove defensive nil check and use deleteResourceO…
vmvarela Mar 23, 2026
10a75b8
fix: undo .golangci.yml changes
vmvarela Mar 25, 2026
7949ae1
fix: after rebase
vmvarela Apr 11, 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
Comment thread
vmvarela marked this conversation as resolved.
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,

Comment thread
vmvarela marked this conversation as resolved.
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)
Comment thread
vmvarela marked this conversation as resolved.
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,
Comment thread
vmvarela marked this conversation as resolved.
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 = &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