diff --git a/docs/data-sources/teams.md b/docs/data-sources/teams.md
new file mode 100644
index 000000000..8afbaadfb
--- /dev/null
+++ b/docs/data-sources/teams.md
@@ -0,0 +1,78 @@
+---
+page_title: "octopusdeploy_teams Data Source - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+ Provides information about existing users.
+---
+
+# Data Source `octopusdeploy_teams`
+
+Provides information about existing users.
+
+
+
+## Schema
+
+### Optional
+
+- **ids** (List of String, Optional) A filter to search by a list of IDs.
+- **include_system** (Boolean, Optional) A filter to include system teams.
+- **partial_name** (String, Optional) A filter to search by the partial match of a name.
+- **skip** (Number, Optional) A filter to specify the number of items to skip in the response.
+- **take** (Number, Optional) A filter to specify the number of items to take (or return) in the response.
+
+### Read-only
+
+- **id** (String, Read-only) A auto-generated identifier that includes the timestamp when this data source was last modified.
+- **spaces** (Block List) A list of spaces that match the filter(s). (see [below for nested schema](#nestedblock--spaces))
+- **teams** (Block List) A list of teams that match the filter(s). (see [below for nested schema](#nestedblock--teams))
+
+
+### Nested Schema for `spaces`
+
+Read-only:
+
+- **can_be_deleted** (Boolean, Read-only)
+- **can_be_renamed** (Boolean, Read-only)
+- **can_change_members** (Boolean, Read-only)
+- **can_change_roles** (Boolean, Read-only)
+- **description** (String, Read-only)
+- **external_security_groups** (List of Object, Read-only) (see [below for nested schema](#nestedatt--spaces--external_security_groups))
+- **id** (String, Read-only) The unique ID for this resource.
+- **name** (String, Read-only)
+- **space_id** (String, Read-only)
+- **users** (List of String, Read-only) A list of user IDs designated to be members of this team.
+
+
+### Nested Schema for `spaces.external_security_groups`
+
+- **display_id_and_name** (Boolean)
+- **display_name** (String)
+- **id** (String)
+
+
+
+
+### Nested Schema for `teams`
+
+Read-only:
+
+- **can_be_deleted** (Boolean, Read-only)
+- **can_be_renamed** (Boolean, Read-only)
+- **can_change_members** (Boolean, Read-only)
+- **can_change_roles** (Boolean, Read-only)
+- **description** (String, Read-only)
+- **external_security_groups** (List of Object, Read-only) (see [below for nested schema](#nestedatt--teams--external_security_groups))
+- **id** (String, Read-only) The unique ID for this resource.
+- **name** (String, Read-only)
+- **space_id** (String, Read-only)
+- **users** (List of String, Read-only) A list of user IDs designated to be members of this team.
+
+
+### Nested Schema for `teams.external_security_groups`
+
+- **display_id_and_name** (Boolean)
+- **display_name** (String)
+- **id** (String)
+
+
diff --git a/docs/resources/team.md b/docs/resources/team.md
new file mode 100644
index 000000000..03b3f961e
--- /dev/null
+++ b/docs/resources/team.md
@@ -0,0 +1,44 @@
+---
+page_title: "octopusdeploy_team Resource - terraform-provider-octopusdeploy"
+subcategory: ""
+description: |-
+ This resource manages teams in Octopus Deploy.
+---
+
+# Resource `octopusdeploy_team`
+
+This resource manages teams in Octopus Deploy.
+
+
+
+## Schema
+
+### Required
+
+- **name** (String, Required)
+
+### Optional
+
+- **can_be_deleted** (Boolean, Optional)
+- **can_be_renamed** (Boolean, Optional)
+- **can_change_members** (Boolean, Optional)
+- **can_change_roles** (Boolean, Optional)
+- **description** (String, Optional)
+- **external_security_groups** (Block List) (see [below for nested schema](#nestedblock--external_security_groups))
+- **id** (String, Optional) The unique ID for this resource.
+- **users** (List of String, Optional) A list of user IDs designated to be members of this team.
+
+### Read-only
+
+- **space_id** (String, Read-only)
+
+
+### Nested Schema for `external_security_groups`
+
+Optional:
+
+- **display_id_and_name** (Boolean, Optional)
+- **display_name** (String, Optional)
+- **id** (String, Optional) The unique ID for this resource.
+
+
diff --git a/octopusdeploy/data_source_teams.go b/octopusdeploy/data_source_teams.go
new file mode 100644
index 000000000..27e808041
--- /dev/null
+++ b/octopusdeploy/data_source_teams.go
@@ -0,0 +1,44 @@
+package octopusdeploy
+
+import (
+ "context"
+ "time"
+
+ "github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func dataSourceTeams() *schema.Resource {
+ return &schema.Resource{
+ Description: "Provides information about existing users.",
+ ReadContext: dataSourceTeamsRead,
+ Schema: getTeamDataSchema(),
+ }
+}
+
+func dataSourceTeamsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ query := octopusdeploy.TeamsQuery{
+ IDs: expandArray(d.Get("ids").([]interface{})),
+ IncludeSystem: d.Get("include_system").(bool),
+ PartialName: d.Get("partial_name").(string),
+ Skip: d.Get("skip").(int),
+ Take: d.Get("take").(int),
+ }
+
+ client := meta.(*octopusdeploy.Client)
+ users, err := client.Teams.Get(query)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ flattenedTeams := []interface{}{}
+ for _, user := range users.Items {
+ flattenedTeams = append(flattenedTeams, flattenTeam(user))
+ }
+
+ d.Set("teams", flattenedTeams)
+ d.SetId("Teams " + time.Now().UTC().String())
+
+ return nil
+}
diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go
index 031d6ce55..a1c5eb98e 100644
--- a/octopusdeploy/provider.go
+++ b/octopusdeploy/provider.go
@@ -35,6 +35,7 @@ func Provider() *schema.Provider {
"octopusdeploy_spaces": dataSourceSpaces(),
"octopusdeploy_ssh_connection_deployment_targets": dataSourceSSHConnectionDeploymentTargets(),
"octopusdeploy_tag_sets": dataSourceTagSets(),
+ "octopusdeploy_teams": dataSourceTeams(),
"octopusdeploy_tenants": dataSourceTenants(),
"octopusdeploy_users": dataSourceUsers(),
"octopusdeploy_user_roles": dataSourceUserRoles(),
@@ -70,6 +71,7 @@ func Provider() *schema.Provider {
"octopusdeploy_ssh_connection_deployment_target": resourceSSHConnectionDeploymentTarget(),
"octopusdeploy_ssh_key_account": resourceSSHKeyAccount(),
"octopusdeploy_tag_set": resourceTagSet(),
+ "octopusdeploy_team": resourceTeam(),
"octopusdeploy_tenant": resourceTenant(),
"octopusdeploy_token_account": resourceTokenAccount(),
"octopusdeploy_user": resourceUser(),
diff --git a/octopusdeploy/resource_team.go b/octopusdeploy/resource_team.go
new file mode 100644
index 000000000..669b092e7
--- /dev/null
+++ b/octopusdeploy/resource_team.go
@@ -0,0 +1,98 @@
+package octopusdeploy
+
+import (
+ "context"
+ "log"
+
+ "github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func resourceTeam() *schema.Resource {
+ return &schema.Resource{
+ CreateContext: resourceTeamCreate,
+ DeleteContext: resourceTeamDelete,
+ Description: "This resource manages teams in Octopus Deploy.",
+ Importer: getImporter(),
+ ReadContext: resourceTeamRead,
+ Schema: getTeamSchema(),
+ UpdateContext: resourceTeamUpdate,
+ }
+}
+
+func resourceTeamCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ team := expandTeam(d)
+
+ log.Printf("[INFO] creating team: %#v", team)
+
+ client := m.(*octopusdeploy.Client)
+ createdTeam, err := client.Teams.Add(team)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ if err := setTeam(ctx, d, createdTeam); err != nil {
+ return diag.FromErr(err)
+ }
+
+ d.SetId(createdTeam.GetID())
+
+ log.Printf("[INFO] team created (%s)", d.Id())
+ return nil
+}
+
+func resourceTeamDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ log.Printf("[INFO] deleting team (%s)", d.Id())
+
+ client := m.(*octopusdeploy.Client)
+ if err := client.Teams.DeleteByID(d.Id()); err != nil {
+ return diag.FromErr(err)
+ }
+
+ d.SetId("")
+
+ log.Printf("[INFO] team deleted")
+ return nil
+}
+
+func resourceTeamRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ log.Printf("[INFO] reading team (%s)", d.Id())
+
+ client := m.(*octopusdeploy.Client)
+ team, err := client.Teams.GetByID(d.Id())
+ if err != nil {
+ apiError := err.(*octopusdeploy.APIError)
+ if apiError.StatusCode == 404 {
+ log.Printf("[INFO] team (%s) not found; deleting from state", d.Id())
+ d.SetId("")
+ return nil
+ }
+ return diag.FromErr(err)
+ }
+
+ if err := setTeam(ctx, d, team); err != nil {
+ return diag.FromErr(err)
+ }
+
+ log.Printf("[INFO] team read (%s)", d.Id())
+ return nil
+}
+
+func resourceTeamUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ log.Printf("[INFO] updating team (%s)", d.Id())
+
+ team := expandTeam(d)
+ client := m.(*octopusdeploy.Client)
+ updatedTeam, err := client.Teams.Update(team)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ if err := setTeam(ctx, d, updatedTeam); err != nil {
+ return diag.FromErr(err)
+ }
+
+ log.Printf("[INFO] team updated (%s)", d.Id())
+ return nil
+}
diff --git a/octopusdeploy/schema_external_security_groups.go b/octopusdeploy/schema_external_security_groups.go
new file mode 100644
index 000000000..acf7b5086
--- /dev/null
+++ b/octopusdeploy/schema_external_security_groups.go
@@ -0,0 +1,73 @@
+package octopusdeploy
+
+import (
+ "github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func expandExternalSecurityGroups(externalSecurityGroups []interface{}) []octopusdeploy.NamedReferenceItem {
+ expandedExternalSecurityGroups := make([]octopusdeploy.NamedReferenceItem, 0, len(externalSecurityGroups))
+ for _, externalSecurityGroup := range externalSecurityGroups {
+ if externalSecurityGroup != nil {
+ rawExternalSecurityGroup := externalSecurityGroup.(map[string]interface{})
+
+ displayIDAndName := false
+ if rawExternalSecurityGroup["display_id_and_name"] != nil {
+ displayIDAndName = rawExternalSecurityGroup["display_id_and_name"].(bool)
+ }
+
+ displayName := ""
+ if rawExternalSecurityGroup["display_name"] != nil {
+ displayName = rawExternalSecurityGroup["display_name"].(string)
+ }
+
+ id := ""
+ if rawExternalSecurityGroup["id"] != nil {
+ id = rawExternalSecurityGroup["id"].(string)
+ }
+
+ item := octopusdeploy.NamedReferenceItem{
+ DisplayIDAndName: displayIDAndName,
+ DisplayName: displayName,
+ ID: id,
+ }
+ expandedExternalSecurityGroups = append(expandedExternalSecurityGroups, item)
+ }
+ }
+ return expandedExternalSecurityGroups
+}
+
+func flattenExternalSecurityGroups(externalSecurityGroups []octopusdeploy.NamedReferenceItem) []interface{} {
+ if externalSecurityGroups == nil {
+ return nil
+ }
+
+ flattenedExternalSecurityGroups := make([]interface{}, len(externalSecurityGroups))
+ for i, externalSecurityGroup := range externalSecurityGroups {
+ rawExternalSecurityGroup := map[string]interface{}{
+ "display_id_and_name": externalSecurityGroup.DisplayIDAndName,
+ "display_name": externalSecurityGroup.DisplayName,
+ "id": externalSecurityGroup.ID,
+ }
+
+ flattenedExternalSecurityGroups[i] = rawExternalSecurityGroup
+ }
+
+ return flattenedExternalSecurityGroups
+}
+
+func getExternalSecurityGroupsSchema() map[string]*schema.Schema {
+ return map[string]*schema.Schema{
+ "display_id_and_name": {
+ Computed: true,
+ Optional: true,
+ Type: schema.TypeBool,
+ },
+ "display_name": {
+ Computed: true,
+ Optional: true,
+ Type: schema.TypeString,
+ },
+ "id": getIDSchema(),
+ }
+}
diff --git a/octopusdeploy/schema_queries.go b/octopusdeploy/schema_queries.go
index 95d9f166e..f2be184d3 100644
--- a/octopusdeploy/schema_queries.go
+++ b/octopusdeploy/schema_queries.go
@@ -165,6 +165,14 @@ func getQueryIDs() *schema.Schema {
}
}
+func getQueryIncludeSystem() *schema.Schema {
+ return &schema.Schema{
+ Description: "A filter to include system teams.",
+ Optional: true,
+ Type: schema.TypeBool,
+ }
+}
+
func getQueryIsClone() *schema.Schema {
return &schema.Schema{
Description: "A filter to search for cloned resources.",
diff --git a/octopusdeploy/schema_team.go b/octopusdeploy/schema_team.go
new file mode 100644
index 000000000..873fa6123
--- /dev/null
+++ b/octopusdeploy/schema_team.go
@@ -0,0 +1,170 @@
+package octopusdeploy
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func expandTeam(d *schema.ResourceData) *octopusdeploy.Team {
+ name := d.Get("name").(string)
+
+ team := octopusdeploy.NewTeam(name)
+ team.ID = d.Id()
+
+ if v, ok := d.GetOk("can_be_deleted"); ok {
+ team.CanBeDeleted = v.(bool)
+ }
+
+ if v, ok := d.GetOk("can_be_renamed"); ok {
+ team.CanBeRenamed = v.(bool)
+ }
+
+ if v, ok := d.GetOk("can_change_members"); ok {
+ team.CanChangeMembers = v.(bool)
+ }
+
+ if v, ok := d.GetOk("can_change_roles"); ok {
+ team.CanChangeRoles = v.(bool)
+ }
+
+ if v, ok := d.GetOk("description"); ok {
+ team.Description = v.(string)
+ }
+
+ if v, ok := d.GetOk("external_security_groups"); ok {
+ team.ExternalSecurityGroups = expandExternalSecurityGroups(v.(*schema.Set).List())
+ }
+
+ if v, ok := d.GetOk("space_id"); ok {
+ team.SpaceID = v.(string)
+ }
+
+ if v, ok := d.GetOk("users"); ok {
+ team.MemberUserIDs = getSliceFromTerraformTypeList(v)
+ }
+
+ return team
+}
+
+func flattenTeam(team *octopusdeploy.Team) map[string]interface{} {
+ if team == nil {
+ return nil
+ }
+
+ return map[string]interface{}{
+ "can_be_deleted": team.CanBeDeleted,
+ "can_be_renamed": team.CanBeRenamed,
+ "can_change_members": team.CanChangeMembers,
+ "can_change_roles": team.CanChangeRoles,
+ "description": team.Description,
+ "external_security_groups": flattenExternalSecurityGroups(team.ExternalSecurityGroups),
+ "id": team.GetID(),
+ "name": team.Name,
+ "space_id": team.SpaceID,
+ "users": team.MemberUserIDs,
+ }
+}
+
+func getTeamDataSchema() map[string]*schema.Schema {
+ dataSchema := getTeamSchema()
+ setDataSchema(&dataSchema)
+
+ return map[string]*schema.Schema{
+ "id": getDataSchemaID(),
+ "ids": getQueryIDs(),
+ "include_system": getQueryIncludeSystem(),
+ "partial_name": getQueryPartialName(),
+ "skip": getQuerySkip(),
+ "spaces": {
+ Description: "A list of spaces that match the filter(s).",
+ Elem: &schema.Resource{Schema: dataSchema},
+ Optional: true,
+ Type: schema.TypeList,
+ },
+ "take": getQueryTake(),
+ "teams": {
+ Computed: true,
+ Description: "A list of teams that match the filter(s).",
+ Elem: &schema.Resource{Schema: dataSchema},
+ Optional: true,
+ Type: schema.TypeList,
+ },
+ }
+}
+
+func getTeamSchema() map[string]*schema.Schema {
+ return map[string]*schema.Schema{
+ "can_be_deleted": {
+ Computed: true,
+ Optional: true,
+ Type: schema.TypeBool,
+ },
+ "can_be_renamed": {
+ Computed: true,
+ Optional: true,
+ Type: schema.TypeBool,
+ },
+ "can_change_members": {
+ Computed: true,
+ Optional: true,
+ Type: schema.TypeBool,
+ },
+ "can_change_roles": {
+ Computed: true,
+ Optional: true,
+ Type: schema.TypeBool,
+ },
+ "description": {
+ Optional: true,
+ Type: schema.TypeString,
+ },
+ "external_security_groups": {
+ Optional: true,
+ Elem: &schema.Resource{Schema: getExternalSecurityGroupsSchema()},
+ Type: schema.TypeList,
+ },
+ "id": getIDSchema(),
+ "name": {
+ Required: true,
+ Type: schema.TypeString,
+ },
+ "space_id": {
+ Computed: true,
+ Type: schema.TypeString,
+ },
+ "users": {
+ Computed: true,
+ Description: "A list of user IDs designated to be members of this team.",
+ Elem: &schema.Schema{Type: schema.TypeString},
+ Optional: true,
+ Type: schema.TypeList,
+ },
+ }
+}
+
+func setTeam(ctx context.Context, d *schema.ResourceData, team *octopusdeploy.Team) error {
+ d.Set("can_be_deleted", team.CanBeDeleted)
+ d.Set("can_be_renamed", team.CanBeRenamed)
+ d.Set("can_change_members", team.CanChangeMembers)
+ d.Set("can_change_roles", team.CanChangeRoles)
+ d.Set("description", team.Description)
+
+ if err := d.Set("external_security_groups", flattenExternalSecurityGroups(team.ExternalSecurityGroups)); err != nil {
+ return fmt.Errorf("error setting external_security_groups: %s", err)
+ }
+
+ d.Set("id", team.GetID())
+ d.Set("name", team.Name)
+ d.Set("space_id", team.SpaceID)
+
+ if err := d.Set("users", team.MemberUserIDs); err != nil {
+ return fmt.Errorf("error setting users: %s", err)
+ }
+
+ d.SetId(team.GetID())
+
+ return nil
+}