Skip to content

Commit 52a3d8a

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 71bbe3f commit 52a3d8a

13 files changed

+1186
-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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
)
10+
11+
func dataSourceGithubEnterpriseCostCenter() *schema.Resource {
12+
return &schema.Resource{
13+
Description: "Use this data source to retrieve information about a specific enterprise cost center.",
14+
ReadContext: dataSourceGithubEnterpriseCostCenterRead,
15+
16+
Schema: map[string]*schema.Schema{
17+
"enterprise_slug": {
18+
Type: schema.TypeString,
19+
Required: true,
20+
Description: "The slug of the enterprise.",
21+
},
22+
"cost_center_id": {
23+
Type: schema.TypeString,
24+
Required: true,
25+
Description: "The ID of the cost center.",
26+
},
27+
"name": {
28+
Type: schema.TypeString,
29+
Computed: true,
30+
Description: "The name of the cost center.",
31+
},
32+
"state": {
33+
Type: schema.TypeString,
34+
Computed: true,
35+
Description: "The state of the cost center.",
36+
},
37+
"azure_subscription": {
38+
Type: schema.TypeString,
39+
Computed: true,
40+
Description: "The Azure subscription associated with the cost center.",
41+
},
42+
"users": {
43+
Type: schema.TypeSet,
44+
Computed: true,
45+
Elem: &schema.Schema{Type: schema.TypeString},
46+
Description: "The usernames assigned to this cost center.",
47+
},
48+
"organizations": {
49+
Type: schema.TypeSet,
50+
Computed: true,
51+
Elem: &schema.Schema{Type: schema.TypeString},
52+
Description: "The organization logins assigned to this cost center.",
53+
},
54+
"repositories": {
55+
Type: schema.TypeSet,
56+
Computed: true,
57+
Elem: &schema.Schema{Type: schema.TypeString},
58+
Description: "The repositories (full name) assigned to this cost center.",
59+
},
60+
},
61+
}
62+
}
63+
64+
func dataSourceGithubEnterpriseCostCenterRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
65+
client := meta.(*Owner).v3client
66+
enterpriseSlug := d.Get("enterprise_slug").(string)
67+
costCenterID := d.Get("cost_center_id").(string)
68+
69+
cc, _, err := client.Enterprise.GetCostCenter(ctx, enterpriseSlug, costCenterID)
70+
if err != nil {
71+
return diag.FromErr(err)
72+
}
73+
74+
d.SetId(costCenterID)
75+
if err := d.Set("name", cc.Name); err != nil {
76+
return diag.FromErr(err)
77+
}
78+
79+
state := strings.ToLower(cc.GetState())
80+
if state == "" {
81+
state = "active"
82+
}
83+
if err := d.Set("state", state); err != nil {
84+
return diag.FromErr(err)
85+
}
86+
if err := d.Set("azure_subscription", cc.GetAzureSubscription()); err != nil {
87+
return diag.FromErr(err)
88+
}
89+
90+
if err := setCostCenterResourceFields(d, cc); err != nil {
91+
return diag.FromErr(err)
92+
}
93+
94+
return nil
95+
}
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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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: toDiagFunc(validation.StringInSlice([]string{"active", "deleted"}, false), "state"),
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+
s := v.(string)
68+
state = &s
69+
}
70+
71+
result, _, err := client.Enterprise.ListCostCenters(ctx, enterpriseSlug, &github.ListCostCenterOptions{State: state})
72+
if err != nil {
73+
return diag.FromErr(err)
74+
}
75+
76+
items := make([]any, 0, len(result.CostCenters))
77+
for _, cc := range result.CostCenters {
78+
if cc == nil {
79+
continue
80+
}
81+
items = append(items, map[string]any{
82+
"id": cc.ID,
83+
"name": cc.Name,
84+
"state": cc.GetState(),
85+
"azure_subscription": cc.GetAzureSubscription(),
86+
})
87+
}
88+
89+
stateStr := ""
90+
if state != nil {
91+
stateStr = *state
92+
}
93+
d.SetId(buildTwoPartID(enterpriseSlug, stateStr))
94+
if err := d.Set("cost_centers", items); err != nil {
95+
return diag.FromErr(err)
96+
}
97+
return nil
98+
}
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+
}

0 commit comments

Comments
 (0)