Skip to content

Commit 2763ff4

Browse files
feat: add support for Enterprise Cost Centers
- Add github_enterprise_cost_center resource for managing cost centers - Add github_enterprise_cost_center data source for individual lookup - Add github_enterprise_cost_centers data source for listing all cost centers - Support user, organization, and repository assignments - Implement chunked API requests for bulk operations - Add acceptance tests using testAccConf.username - Register resources and data sources in provider
1 parent 71bbe3f commit 2763ff4

13 files changed

+1118
-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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"sort"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func dataSourceGithubEnterpriseCostCenter() *schema.Resource {
13+
return &schema.Resource{
14+
Description: "Use this data source to retrieve information about a specific enterprise cost center.",
15+
ReadContext: dataSourceGithubEnterpriseCostCenterRead,
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+
"cost_center_id": {
24+
Type: schema.TypeString,
25+
Required: true,
26+
Description: "The ID of the cost center.",
27+
},
28+
"name": {
29+
Type: schema.TypeString,
30+
Computed: true,
31+
Description: "The name of the cost center.",
32+
},
33+
"state": {
34+
Type: schema.TypeString,
35+
Computed: true,
36+
Description: "The state of the cost center.",
37+
},
38+
"azure_subscription": {
39+
Type: schema.TypeString,
40+
Computed: true,
41+
Description: "The Azure subscription associated with the cost center.",
42+
},
43+
"users": {
44+
Type: schema.TypeSet,
45+
Computed: true,
46+
Elem: &schema.Schema{Type: schema.TypeString},
47+
Description: "The usernames assigned to this cost center.",
48+
},
49+
"organizations": {
50+
Type: schema.TypeSet,
51+
Computed: true,
52+
Elem: &schema.Schema{Type: schema.TypeString},
53+
Description: "The organization logins assigned to this cost center.",
54+
},
55+
"repositories": {
56+
Type: schema.TypeSet,
57+
Computed: true,
58+
Elem: &schema.Schema{Type: schema.TypeString},
59+
Description: "The repositories (full name) assigned to this cost center.",
60+
},
61+
},
62+
}
63+
}
64+
65+
func dataSourceGithubEnterpriseCostCenterRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
66+
client := meta.(*Owner).v3client
67+
enterpriseSlug := d.Get("enterprise_slug").(string)
68+
costCenterID := d.Get("cost_center_id").(string)
69+
70+
cc, _, err := client.Enterprise.GetCostCenter(ctx, enterpriseSlug, costCenterID)
71+
if err != nil {
72+
return diag.FromErr(err)
73+
}
74+
75+
d.SetId(costCenterID)
76+
_ = d.Set("name", cc.Name)
77+
78+
state := strings.ToLower(cc.GetState())
79+
if state == "" {
80+
state = "active"
81+
}
82+
_ = d.Set("state", state)
83+
_ = d.Set("azure_subscription", cc.GetAzureSubscription())
84+
85+
users, organizations, repositories := costCenterSplitResources(cc.Resources)
86+
sort.Strings(users)
87+
sort.Strings(organizations)
88+
sort.Strings(repositories)
89+
_ = d.Set("users", flattenStringList(users))
90+
_ = d.Set("organizations", flattenStringList(organizations))
91+
_ = d.Set("repositories", flattenStringList(repositories))
92+
93+
return nil
94+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
13+
14+
if testAccConf.username == "" {
15+
t.Skip("Skipping because `GITHUB_USERNAME` is not set")
16+
}
17+
18+
// Use username for testing
19+
user := testAccConf.username
20+
21+
config := fmt.Sprintf(`
22+
data "github_enterprise" "enterprise" {
23+
slug = "%s"
24+
}
25+
26+
resource "github_enterprise_cost_center" "test" {
27+
enterprise_slug = data.github_enterprise.enterprise.slug
28+
name = "tf-acc-test-%s"
29+
30+
users = [%q]
31+
}
32+
33+
data "github_enterprise_cost_center" "test" {
34+
enterprise_slug = data.github_enterprise.enterprise.slug
35+
cost_center_id = github_enterprise_cost_center.test.id
36+
}
37+
`, testAccConf.enterpriseSlug, randomID, user)
38+
39+
resource.Test(t, resource.TestCase{
40+
PreCheck: func() { skipUnlessEnterprise(t) },
41+
ProviderFactories: providerFactories,
42+
Steps: []resource.TestStep{{
43+
Config: config,
44+
Check: resource.ComposeTestCheckFunc(
45+
resource.TestCheckResourceAttrPair("data.github_enterprise_cost_center.test", "cost_center_id", "github_enterprise_cost_center.test", "id"),
46+
resource.TestCheckResourceAttrPair("data.github_enterprise_cost_center.test", "name", "github_enterprise_cost_center.test", "name"),
47+
resource.TestCheckResourceAttr("data.github_enterprise_cost_center.test", "state", "active"),
48+
resource.TestCheckResourceAttr("data.github_enterprise_cost_center.test", "users.#", "1"),
49+
resource.TestCheckTypeSetElemAttr("data.github_enterprise_cost_center.test", "users.*", user),
50+
),
51+
}},
52+
})
53+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
_ = d.Set("cost_centers", items)
95+
return nil
96+
}

0 commit comments

Comments
 (0)