Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
data "github_copilot_organization_settings" "example" {}

output "seat_management" {
value = data.github_copilot_organization_settings.example.seat_management_setting
}

output "total_seats" {
value = data.github_copilot_organization_settings.example.seat_breakdown[0].total
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "github_copilot_organization_seat_assignment" "example" {
username = "someuser"
}
7 changes: 7 additions & 0 deletions examples/resources/copilot_team_seat_assignment/example_1.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "github_team" "example" {
name = "my-copilot-team"
}

resource "github_copilot_team_seat_assignment" "example" {
team = github_team.example.slug
}
16 changes: 16 additions & 0 deletions github/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,19 @@ func skipUnlessMode(t *testing.T, testModes ...testMode) {
t.Skip("Skipping as not supported test mode")
}
}

func skipUnlessCopilotSeatManagementEnabled(t *testing.T) {
t.Helper()
skipUnlessHasOrgs(t)
meta, err := getTestMeta()
if err != nil {
t.Fatalf("failed to get test meta: %s", err)
}
billing, _, err := meta.v3client.Copilot.GetCopilotBilling(context.Background(), meta.name)
if err != nil {
t.Skipf("Skipping: Copilot billing not available for org %s: %s", meta.name, err)
}
if billing.GetSeatManagementSetting() != "assign_selected" {
t.Skipf("Skipping: Copilot seat management is %q, must be %q", billing.GetSeatManagementSetting(), "assign_selected")
}
}
106 changes: 106 additions & 0 deletions github/data_source_github_copilot_organization_settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package github

import (
"context"
"errors"
"net/http"

"github.com/google/go-github/v88/github"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceGithubCopilotOrganizationSettings() *schema.Resource {
return &schema.Resource{
ReadContext: dataSourceGithubCopilotOrganizationSettingsRead,

Schema: map[string]*schema.Schema{
"seat_management_setting": {
Type: schema.TypeString,
Computed: true,
Description: "How Copilot seats are assigned: assign_selected, all_members, or unconfigured.",
},
"public_code_suggestions": {
Type: schema.TypeString,
Computed: true,
Description: "Whether Copilot can suggest code matching public repositories: allow or block.",
},
"seat_breakdown": {
Type: schema.TypeList,
Computed: true,
Description: "Breakdown of Copilot seat usage.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"total": {
Type: schema.TypeInt,
Computed: true,
Description: "Total number of Copilot seats.",
},
"added_this_cycle": {
Type: schema.TypeInt,
Computed: true,
Description: "Seats added in the current billing cycle.",
},
"pending_invitation": {
Type: schema.TypeInt,
Computed: true,
Description: "Seats pending invitation acceptance.",
},
"pending_cancellation": {
Type: schema.TypeInt,
Computed: true,
Description: "Seats pending cancellation.",
},
"active_this_cycle": {
Type: schema.TypeInt,
Computed: true,
Description: "Seats active in the current billing cycle.",
},
"inactive_this_cycle": {
Type: schema.TypeInt,
Computed: true,
Description: "Seats inactive in the current billing cycle.",
},
},
},
},
},
}
}

func dataSourceGithubCopilotOrganizationSettingsRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta := m.(*Owner)
client := meta.v3client
org := meta.name

billing, _, err := client.Copilot.GetCopilotBilling(ctx, org)
if err != nil {
if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound {
return diag.Errorf("Copilot is not enabled for organization %q", org)
}
return diag.FromErr(err)
}

d.SetId(org)

if err := d.Set("seat_management_setting", billing.GetSeatManagementSetting()); err != nil {
return diag.FromErr(err)
}
if err := d.Set("public_code_suggestions", billing.GetPublicCodeSuggestions()); err != nil {
return diag.FromErr(err)
}
if breakdown := billing.GetSeatBreakdown(); breakdown != nil {
if err := d.Set("seat_breakdown", []any{map[string]any{
"total": breakdown.GetTotal(),
"added_this_cycle": breakdown.GetAddedThisCycle(),
"pending_invitation": breakdown.GetPendingInvitation(),
"pending_cancellation": breakdown.GetPendingCancellation(),
"active_this_cycle": breakdown.GetActiveThisCycle(),
"inactive_this_cycle": breakdown.GetInactiveThisCycle(),
}}); err != nil {
return diag.FromErr(err)
}
}

return nil
}
25 changes: 25 additions & 0 deletions github/data_source_github_copilot_organization_settings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package github

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccDataSourceGithubCopilotOrganizationSettings(t *testing.T) {
t.Run("reads Copilot organization settings", func(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: `data "github_copilot_organization_settings" "test" {}`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("data.github_copilot_organization_settings.test", "seat_management_setting"),
resource.TestCheckResourceAttrSet("data.github_copilot_organization_settings.test", "public_code_suggestions"),
),
},
},
})
})
}
3 changes: 3 additions & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ func NewProvider() func() *schema.Provider {
"github_codespaces_organization_secret_repositories": resourceGithubCodespacesOrganizationSecretRepositories(),
"github_codespaces_secret": resourceGithubCodespacesSecret(),
"github_codespaces_user_secret": resourceGithubCodespacesUserSecret(),
"github_copilot_organization_seat_assignment": resourceGithubCopilotOrganizationSeatAssignment(),
"github_copilot_team_seat_assignment": resourceGithubCopilotTeamSeatAssignment(),
"github_dependabot_organization_secret": resourceGithubDependabotOrganizationSecret(),
"github_dependabot_organization_secret_repositories": resourceGithubDependabotOrganizationSecretRepositories(),
"github_dependabot_organization_secret_repository": resourceGithubDependabotOrganizationSecretRepository(),
Expand Down Expand Up @@ -267,6 +269,7 @@ func NewProvider() func() *schema.Provider {
"github_codespaces_secrets": dataSourceGithubCodespacesSecrets(),
"github_codespaces_user_public_key": dataSourceGithubCodespacesUserPublicKey(),
"github_codespaces_user_secrets": dataSourceGithubCodespacesUserSecrets(),
"github_copilot_organization_settings": dataSourceGithubCopilotOrganizationSettings(),
"github_dependabot_organization_public_key": dataSourceGithubDependabotOrganizationPublicKey(),
"github_dependabot_organization_secrets": dataSourceGithubDependabotOrganizationSecrets(),
"github_dependabot_public_key": dataSourceGithubDependabotPublicKey(),
Expand Down
138 changes: 138 additions & 0 deletions github/resource_github_copilot_organization_seat_assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package github

import (
"context"
"errors"
"net/http"

"github.com/google/go-github/v88/github"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceGithubCopilotOrganizationSeatAssignment() *schema.Resource {
return &schema.Resource{
CreateContext: resourceGithubCopilotOrganizationSeatAssignmentCreate,
ReadContext: resourceGithubCopilotOrganizationSeatAssignmentRead,
DeleteContext: resourceGithubCopilotOrganizationSeatAssignmentDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"username": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: caseInsensitive(),
Description: "The login of the user to assign a Copilot seat.",
},
"created_at": {
Type: schema.TypeString,
Computed: true,
Description: "Timestamp of when the seat was first assigned.",
},
"pending_cancellation_date": {
Type: schema.TypeString,
Computed: true,
Description: "Date on which the seat will be cancelled, if pending cancellation.",
},
"last_activity_at": {
Type: schema.TypeString,
Computed: true,
Description: "Timestamp of the user's last Copilot activity.",
},
"last_activity_editor": {
Type: schema.TypeString,
Computed: true,
Description: "Editor used in the user's last Copilot activity.",
},
"plan_type": {
Type: schema.TypeString,
Computed: true,
Description: "The Copilot plan type for this seat (e.g. business, enterprise).",
},
},
}
}

func resourceGithubCopilotOrganizationSeatAssignmentCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta := m.(*Owner)
client := meta.v3client
org := meta.name

username := d.Get("username").(string)

_, _, err := client.Copilot.AddCopilotUsers(ctx, org, []string{username})
if err != nil {
return diag.FromErr(err)
}

d.SetId(username)

return resourceGithubCopilotOrganizationSeatAssignmentRead(ctx, d, m)
}

func resourceGithubCopilotOrganizationSeatAssignmentRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta := m.(*Owner)
client := meta.v3client
org := meta.name

username := d.Id()

seat, _, err := client.Copilot.GetSeatDetails(ctx, org, username)
if err != nil {
if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound {
tflog.Info(ctx, "Copilot seat assignment no longer exists, removing from state", map[string]any{"username": username})
d.SetId("")
return nil
}
return diag.FromErr(err)
}

if err := d.Set("username", username); err != nil {
return diag.FromErr(err)
}

createdAt := seat.GetCreatedAt()
if err := d.Set("created_at", createdAt.String()); err != nil {
return diag.FromErr(err)
}
if err := d.Set("pending_cancellation_date", seat.GetPendingCancellationDate()); err != nil {
return diag.FromErr(err)
}
if err := d.Set("last_activity_editor", seat.GetLastActivityEditor()); err != nil {
return diag.FromErr(err)
}
if err := d.Set("plan_type", seat.GetPlanType()); err != nil {
return diag.FromErr(err)
}

if lastActivity := seat.GetLastActivityAt(); !lastActivity.IsZero() {
if err := d.Set("last_activity_at", lastActivity.String()); err != nil {
return diag.FromErr(err)
}
}

return nil
}

func resourceGithubCopilotOrganizationSeatAssignmentDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta := m.(*Owner)
client := meta.v3client
org := meta.name

username := d.Id()

_, _, err := client.Copilot.RemoveCopilotUsers(ctx, org, []string{username})
if err != nil {
if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound {
tflog.Info(ctx, "Copilot seat assignment no longer exists, skipping delete", map[string]any{"username": username})
return nil
}
return diag.FromErr(err)
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package github

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccGithubCopilotOrganizationSeatAssignment(t *testing.T) {
if testAccConf.testOrgUser == "" {
t.Skip("GH_TEST_ORG_USER not set")
}

username := testAccConf.testOrgUser

t.Run("assigns and removes a Copilot seat for a user", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_copilot_organization_seat_assignment" "test" {
username = "%s"
}
`, username)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessCopilotSeatManagementEnabled(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("github_copilot_organization_seat_assignment.test", "username", username),
),
},
{
ResourceName: "github_copilot_organization_seat_assignment.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
})
}
Loading