Skip to content

Commit 0143ec1

Browse files
feat: add support for Enterprise Cost Centers
This adds a new resource and data sources for managing GitHub Enterprise Cost Centers via the REST API. New resources: - github_enterprise_cost_center: Create, update, and archive cost centers New data sources: - github_enterprise_cost_center: Retrieve a cost center by ID - github_enterprise_cost_centers: List cost centers with optional state filter Features: - Authoritative management of cost center resource assignments (users, organizations, repositories) - Retry logic with exponential backoff for transient API errors - Batch processing for large resource assignments (max 50 per request) - Import support using enterprise_slug:cost_center_id format Documentation and examples included.
1 parent 15fff78 commit 0143ec1

13 files changed

+1131
-0
lines changed

examples/cost_centers/main.tf

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
terraform {
2+
required_providers {
3+
github = {
4+
source = "integrations/github"
5+
version = "~> 6.0"
6+
}
7+
}
8+
}
9+
10+
provider "github" {
11+
token = var.github_token
12+
owner = var.enterprise_slug
13+
}
14+
15+
variable "github_token" {
16+
description = "GitHub classic personal access token (PAT) for an enterprise admin"
17+
type = string
18+
sensitive = true
19+
}
20+
21+
variable "enterprise_slug" {
22+
description = "The GitHub Enterprise slug"
23+
type = string
24+
}
25+
26+
variable "cost_center_name" {
27+
description = "Name for the cost center"
28+
type = string
29+
}
30+
31+
variable "users" {
32+
description = "Usernames to assign to the cost center"
33+
type = list(string)
34+
default = []
35+
}
36+
37+
variable "organizations" {
38+
description = "Organization logins to assign to the cost center"
39+
type = list(string)
40+
default = []
41+
}
42+
43+
variable "repositories" {
44+
description = "Repositories (full name, e.g. org/repo) to assign to the cost center"
45+
type = list(string)
46+
default = []
47+
}
48+
49+
resource "github_enterprise_cost_center" "example" {
50+
enterprise_slug = var.enterprise_slug
51+
name = var.cost_center_name
52+
53+
# Authoritative assignments: Terraform will add/remove to match these lists.
54+
users = var.users
55+
organizations = var.organizations
56+
repositories = var.repositories
57+
}
58+
59+
data "github_enterprise_cost_center" "by_id" {
60+
enterprise_slug = var.enterprise_slug
61+
cost_center_id = github_enterprise_cost_center.example.id
62+
}
63+
64+
data "github_enterprise_cost_centers" "active" {
65+
enterprise_slug = var.enterprise_slug
66+
state = "active"
67+
68+
depends_on = [github_enterprise_cost_center.example]
69+
}
70+
71+
output "cost_center" {
72+
description = "Created cost center"
73+
value = {
74+
id = github_enterprise_cost_center.example.id
75+
name = github_enterprise_cost_center.example.name
76+
state = github_enterprise_cost_center.example.state
77+
azure_subscription = github_enterprise_cost_center.example.azure_subscription
78+
}
79+
}
80+
81+
output "cost_center_resources" {
82+
description = "Effective assignments (read from API)"
83+
value = {
84+
users = sort(tolist(github_enterprise_cost_center.example.users))
85+
organizations = sort(tolist(github_enterprise_cost_center.example.organizations))
86+
repositories = sort(tolist(github_enterprise_cost_center.example.repositories))
87+
}
88+
}
89+
90+
output "cost_center_from_data_source" {
91+
description = "Cost center fetched by data source"
92+
value = {
93+
id = data.github_enterprise_cost_center.by_id.cost_center_id
94+
name = data.github_enterprise_cost_center.by_id.name
95+
state = data.github_enterprise_cost_center.by_id.state
96+
users = sort(tolist(data.github_enterprise_cost_center.by_id.users))
97+
organizations = sort(tolist(data.github_enterprise_cost_center.by_id.organizations))
98+
repositories = sort(tolist(data.github_enterprise_cost_center.by_id.repositories))
99+
}
100+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
)
9+
10+
func dataSourceGithubEnterpriseCostCenter() *schema.Resource {
11+
return &schema.Resource{
12+
Description: "Use this data source to retrieve information about a specific enterprise cost center.",
13+
ReadContext: dataSourceGithubEnterpriseCostCenterRead,
14+
15+
Schema: map[string]*schema.Schema{
16+
"enterprise_slug": {
17+
Type: schema.TypeString,
18+
Required: true,
19+
Description: "The slug of the enterprise.",
20+
},
21+
"cost_center_id": {
22+
Type: schema.TypeString,
23+
Required: true,
24+
Description: "The ID of the cost center.",
25+
},
26+
"name": {
27+
Type: schema.TypeString,
28+
Computed: true,
29+
Description: "The name of the cost center.",
30+
},
31+
"state": {
32+
Type: schema.TypeString,
33+
Computed: true,
34+
Description: "The state of the cost center.",
35+
},
36+
"azure_subscription": {
37+
Type: schema.TypeString,
38+
Computed: true,
39+
Description: "The Azure subscription associated with the cost center.",
40+
},
41+
"users": {
42+
Type: schema.TypeSet,
43+
Computed: true,
44+
Elem: &schema.Schema{Type: schema.TypeString},
45+
Description: "The usernames assigned to this cost center.",
46+
},
47+
"organizations": {
48+
Type: schema.TypeSet,
49+
Computed: true,
50+
Elem: &schema.Schema{Type: schema.TypeString},
51+
Description: "The organization logins assigned to this cost center.",
52+
},
53+
"repositories": {
54+
Type: schema.TypeSet,
55+
Computed: true,
56+
Elem: &schema.Schema{Type: schema.TypeString},
57+
Description: "The repositories (full name) assigned to this cost center.",
58+
},
59+
},
60+
}
61+
}
62+
63+
func dataSourceGithubEnterpriseCostCenterRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
64+
client := meta.(*Owner).v3client
65+
enterpriseSlug := d.Get("enterprise_slug").(string)
66+
costCenterID := d.Get("cost_center_id").(string)
67+
68+
cc, _, err := client.Enterprise.GetCostCenter(ctx, enterpriseSlug, costCenterID)
69+
if err != nil {
70+
return diag.FromErr(err)
71+
}
72+
73+
d.SetId(costCenterID)
74+
if err := d.Set("name", cc.Name); err != nil {
75+
return diag.FromErr(err)
76+
}
77+
78+
if err := d.Set("state", cc.GetState()); err != nil {
79+
return diag.FromErr(err)
80+
}
81+
if err := d.Set("azure_subscription", cc.GetAzureSubscription()); err != nil {
82+
return diag.FromErr(err)
83+
}
84+
85+
if err := setCostCenterResourceFields(d, cc); err != nil {
86+
return diag.FromErr(err)
87+
}
88+
89+
return nil
90+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
9+
)
10+
11+
func TestAccGithubEnterpriseCostCenterDataSource(t *testing.T) {
12+
randomID := acctest.RandString(5)
13+
user := testAccConf.username
14+
15+
resource.Test(t, resource.TestCase{
16+
PreCheck: func() { skipUnlessEnterprise(t) },
17+
ProviderFactories: providerFactories,
18+
Steps: []resource.TestStep{{
19+
Config: fmt.Sprintf(`
20+
data "github_enterprise" "enterprise" {
21+
slug = "%s"
22+
}
23+
24+
resource "github_enterprise_cost_center" "test" {
25+
enterprise_slug = data.github_enterprise.enterprise.slug
26+
name = "%s%s"
27+
28+
users = [%q]
29+
}
30+
31+
data "github_enterprise_cost_center" "test" {
32+
enterprise_slug = data.github_enterprise.enterprise.slug
33+
cost_center_id = github_enterprise_cost_center.test.id
34+
}
35+
`, testAccConf.enterpriseSlug, testResourcePrefix, randomID, user),
36+
Check: resource.ComposeTestCheckFunc(
37+
resource.TestCheckResourceAttrPair("data.github_enterprise_cost_center.test", "cost_center_id", "github_enterprise_cost_center.test", "id"),
38+
resource.TestCheckResourceAttrPair("data.github_enterprise_cost_center.test", "name", "github_enterprise_cost_center.test", "name"),
39+
resource.TestCheckResourceAttr("data.github_enterprise_cost_center.test", "state", "active"),
40+
resource.TestCheckResourceAttr("data.github_enterprise_cost_center.test", "users.#", "1"),
41+
resource.TestCheckTypeSetElemAttr("data.github_enterprise_cost_center.test", "users.*", user),
42+
),
43+
}},
44+
})
45+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/go-github/v81/github"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
10+
)
11+
12+
func dataSourceGithubEnterpriseCostCenters() *schema.Resource {
13+
return &schema.Resource{
14+
Description: "Use this data source to retrieve a list of enterprise cost centers.",
15+
ReadContext: dataSourceGithubEnterpriseCostCentersRead,
16+
17+
Schema: map[string]*schema.Schema{
18+
"enterprise_slug": {
19+
Type: schema.TypeString,
20+
Required: true,
21+
Description: "The slug of the enterprise.",
22+
},
23+
"state": {
24+
Type: schema.TypeString,
25+
Optional: true,
26+
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"active", "deleted"}, false)),
27+
Description: "Filter cost centers by state.",
28+
},
29+
"cost_centers": {
30+
Type: schema.TypeSet,
31+
Computed: true,
32+
Description: "The list of cost centers.",
33+
Elem: &schema.Resource{
34+
Schema: map[string]*schema.Schema{
35+
"id": {
36+
Type: schema.TypeString,
37+
Computed: true,
38+
Description: "The cost center ID.",
39+
},
40+
"name": {
41+
Type: schema.TypeString,
42+
Computed: true,
43+
Description: "The name of the cost center.",
44+
},
45+
"state": {
46+
Type: schema.TypeString,
47+
Computed: true,
48+
Description: "The state of the cost center.",
49+
},
50+
"azure_subscription": {
51+
Type: schema.TypeString,
52+
Computed: true,
53+
Description: "The Azure subscription associated with the cost center.",
54+
},
55+
},
56+
},
57+
},
58+
},
59+
}
60+
}
61+
62+
func dataSourceGithubEnterpriseCostCentersRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
63+
client := meta.(*Owner).v3client
64+
enterpriseSlug := d.Get("enterprise_slug").(string)
65+
var state *string
66+
if v, ok := d.GetOk("state"); ok {
67+
state = github.Ptr(v.(string))
68+
}
69+
70+
result, _, err := client.Enterprise.ListCostCenters(ctx, enterpriseSlug, &github.ListCostCenterOptions{State: state})
71+
if err != nil {
72+
return diag.FromErr(err)
73+
}
74+
75+
items := make([]any, 0, len(result.CostCenters))
76+
for _, cc := range result.CostCenters {
77+
if cc == nil {
78+
continue
79+
}
80+
items = append(items, map[string]any{
81+
"id": cc.ID,
82+
"name": cc.Name,
83+
"state": cc.GetState(),
84+
"azure_subscription": cc.GetAzureSubscription(),
85+
})
86+
}
87+
88+
stateStr := "all"
89+
if state != nil {
90+
stateStr = *state
91+
}
92+
d.SetId(buildTwoPartID(enterpriseSlug, stateStr))
93+
if err := d.Set("cost_centers", items); err != nil {
94+
return diag.FromErr(err)
95+
}
96+
return nil
97+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
9+
)
10+
11+
func TestAccGithubEnterpriseCostCentersDataSource(t *testing.T) {
12+
randomID := acctest.RandString(5)
13+
14+
config := fmt.Sprintf(`
15+
data "github_enterprise" "enterprise" {
16+
slug = "%s"
17+
}
18+
19+
resource "github_enterprise_cost_center" "test" {
20+
enterprise_slug = data.github_enterprise.enterprise.slug
21+
name = "%s%s"
22+
}
23+
24+
data "github_enterprise_cost_centers" "test" {
25+
enterprise_slug = data.github_enterprise.enterprise.slug
26+
state = "active"
27+
depends_on = [github_enterprise_cost_center.test]
28+
}
29+
`, testAccConf.enterpriseSlug, testResourcePrefix, randomID)
30+
31+
resource.Test(t, resource.TestCase{
32+
PreCheck: func() { skipUnlessEnterprise(t) },
33+
ProviderFactories: providerFactories,
34+
Steps: []resource.TestStep{{
35+
Config: config,
36+
Check: resource.ComposeTestCheckFunc(
37+
resource.TestCheckResourceAttr("data.github_enterprise_cost_centers.test", "state", "active"),
38+
resource.TestCheckTypeSetElemAttrPair("data.github_enterprise_cost_centers.test", "cost_centers.*.id", "github_enterprise_cost_center.test", "id"),
39+
),
40+
}},
41+
})
42+
}

github/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func Provider() *schema.Provider {
214214
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
215215
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
216216
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
217+
"github_enterprise_cost_center": resourceGithubEnterpriseCostCenter(),
217218
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
218219
},
219220

@@ -289,6 +290,8 @@ func Provider() *schema.Provider {
289290
"github_user_external_identity": dataSourceGithubUserExternalIdentity(),
290291
"github_users": dataSourceGithubUsers(),
291292
"github_enterprise": dataSourceGithubEnterprise(),
293+
"github_enterprise_cost_center": dataSourceGithubEnterpriseCostCenter(),
294+
"github_enterprise_cost_centers": dataSourceGithubEnterpriseCostCenters(),
292295
"github_repository_environment_deployment_policies": dataSourceGithubRepositoryEnvironmentDeploymentPolicies(),
293296
},
294297
}

0 commit comments

Comments
 (0)