Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
efab2d8
feat(utils): add shared helper functions for cost centers
github-actions[bot] Feb 4, 2026
7da8d3c
feat(cost-centers): add retry logic utilities for cost center operations
github-actions[bot] Feb 4, 2026
4034934
feat(cost-centers): add github_enterprise_cost_center resource
github-actions[bot] Feb 4, 2026
2c5f59c
feat(cost-centers): add github_enterprise_cost_center_users resource
github-actions[bot] Feb 4, 2026
a1cdf11
feat(cost-centers): add github_enterprise_cost_center_organizations r…
github-actions[bot] Feb 4, 2026
790d3f9
feat(cost-centers): add github_enterprise_cost_center_repositories re…
github-actions[bot] Feb 4, 2026
0169707
feat(cost-centers): add data sources for enterprise cost centers
github-actions[bot] Feb 4, 2026
70ffa0e
feat(cost-centers): register cost center resources and data sources
github-actions[bot] Feb 4, 2026
fd7ee76
docs(cost-centers): add usage example
github-actions[bot] Feb 4, 2026
b381d96
fix(cost-centers): update go-github import to v82
github-actions[bot] Feb 4, 2026
841d1e9
fix: removed linter config, it shouldn't be needed
vmvarela Feb 9, 2026
78d73ae
fix(cost-centers): remove unnecessary expandStringSet function
vmvarela Feb 9, 2026
4396ca4
fix(cost-centers): check deleted state in Read instead of Update
vmvarela Feb 9, 2026
7210fb5
fix(cost-centers): separate Create and Update for users resource
vmvarela Feb 9, 2026
e479cb4
fix(cost-centers): separate Create and Update for organizations resource
vmvarela Feb 9, 2026
c0a1149
fix(cost-centers): separate Create and Update for repositories resource
vmvarela Feb 9, 2026
e4d5a68
fix(cost-centers): correct resource type strings in CheckDestroy func…
vmvarela Feb 9, 2026
32fe45d
refactor(cost-centers): use type constants instead of magic strings
vmvarela Feb 9, 2026
fda96bf
fix(cost-centers): use terraform-plugin-testing imports in test files
vmvarela Feb 14, 2026
196e438
fix(cost-centers): document generic chunk helper lint exception
vmvarela Feb 17, 2026
c38514d
fix: address review - rename single-char variables and remove unneces…
vmvarela Feb 18, 2026
25f43d0
fix: address review - rename maxResourcesPerRequest to maxCostCenterR…
vmvarela Feb 18, 2026
c0d7219
fix: address review - simplify import test with ImportStateIdPrefix
vmvarela Feb 18, 2026
c0ce0cf
fix: address review - add unit tests for errIs404, errIsRetryable, ch…
vmvarela Feb 18, 2026
371f37c
fix: address review - migrate cost center tests to ConfigStateChecks
vmvarela Feb 18, 2026
35e0092
fix(cost-centers): update go-github import from v82 to v83
vmvarela Feb 23, 2026
8395f25
fix(cost-centers): add default 'all' value to state field in cost cen…
vmvarela Feb 23, 2026
757c071
fix(cost-center): populate name on import via API lookup
vmvarela Feb 24, 2026
c27c343
fix(cost-center): align sub-resource IDs with main resource
vmvarela Feb 24, 2026
bd51909
fix(cost-center): validate no existing assignments on Create
vmvarela Feb 24, 2026
9cc1b71
refactor(cost-center): simplify Update diff with map pattern
vmvarela Feb 24, 2026
34031c6
fix(cost-center): Delete removes all linked resources from API
vmvarela Feb 24, 2026
e7db627
fix(cost-center): add missing sub-resources to website sidebar
vmvarela 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
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: "Use this data source to retrieve information about a specific 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/v83/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: "Use this data source to retrieve a list of 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