From 30b32bce93661dde353429e54803c28a4d0b2db9 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Wed, 4 Oct 2023 14:39:29 +0200 Subject: [PATCH] feat: add support for user group resources Signed-off-by: Michal Wasilewski --- docs/resources/user_group.md | 47 +++++ .../spacelift_user_group/resource.tf | 11 ++ spacelift/internal/structs/user_group.go | 12 ++ .../internal/structs/user_group_input.go | 20 ++ spacelift/provider.go | 1 + spacelift/resource_user_group.go | 173 ++++++++++++++++++ 6 files changed, 264 insertions(+) create mode 100644 docs/resources/user_group.md create mode 100644 examples/resources/spacelift_user_group/resource.tf create mode 100644 spacelift/internal/structs/user_group.go create mode 100644 spacelift/internal/structs/user_group_input.go create mode 100644 spacelift/resource_user_group.go diff --git a/docs/resources/user_group.md b/docs/resources/user_group.md new file mode 100644 index 00000000..a87ccb74 --- /dev/null +++ b/docs/resources/user_group.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_user_group Resource - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_user_group represents a Spacelift user group - a collection of users as provided by your Identity Provider (IdP). If you assign permissions to a user group, all users in the group will have those permissions, unless the user's permissions are higher than the group's permissions. +--- + +# spacelift_user_group (Resource) + +`spacelift_user_group` represents a Spacelift **user group** - a collection of users as provided by your Identity Provider (IdP). If you assign permissions to a user group, all users in the group will have those permissions, unless the user's permissions are higher than the group's permissions. + +## Example Usage + +```terraform +resource "spacelift_user_group" "test" { + name = "test" + access { + space_id = "root" + level = "ADMIN" + } + access { + space_id = "legacy" + level = "ADMIN" + } +} +``` + + +## Schema + +### Required + +- `access` (Block List, Min: 1) (see [below for nested schema](#nestedblock--access)) +- `name` (String) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `access` + +Required: + +- `level` (String) +- `space_id` (String) diff --git a/examples/resources/spacelift_user_group/resource.tf b/examples/resources/spacelift_user_group/resource.tf new file mode 100644 index 00000000..15f72dc4 --- /dev/null +++ b/examples/resources/spacelift_user_group/resource.tf @@ -0,0 +1,11 @@ +resource "spacelift_user_group" "test" { + name = "test" + access { + space_id = "root" + level = "ADMIN" + } + access { + space_id = "legacy" + level = "ADMIN" + } +} diff --git a/spacelift/internal/structs/user_group.go b/spacelift/internal/structs/user_group.go new file mode 100644 index 00000000..054951d4 --- /dev/null +++ b/spacelift/internal/structs/user_group.go @@ -0,0 +1,12 @@ +package structs + +type SpaceAccessRule struct { + Space string `graphql:"space"` + SpaceAccessLevel string `graphql:"spaceAccessLevel"` +} + +type UserGroup struct { + ID string `graphql:"id"` + Name string `graphql:"groupName"` + AccessRules []SpaceAccessRule `graphql:"accessRules"` +} diff --git a/spacelift/internal/structs/user_group_input.go b/spacelift/internal/structs/user_group_input.go new file mode 100644 index 00000000..78d2ac4f --- /dev/null +++ b/spacelift/internal/structs/user_group_input.go @@ -0,0 +1,20 @@ +package structs + +import "github.com/shurcooL/graphql" + +type SpaceAccessLevel string + +type SpaceAccessRuleInput struct { + Space graphql.ID `json:"space"` + SpaceAccessLevel SpaceAccessLevel `json:"spaceAccessLevel"` +} + +type ManagedUserGroupCreateInput struct { + Name graphql.String `json:"groupName"` + AccessRules []SpaceAccessRuleInput `json:"accessRules"` +} + +type ManagedUserGroupUpdateInput struct { + ID graphql.ID `json:"id"` + AccessRules []SpaceAccessRuleInput `json:"accessRules"` +} diff --git a/spacelift/provider.go b/spacelift/provider.go index b38c1025..79d0c2cc 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -125,6 +125,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated "spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated "spacelift_terraform_provider": resourceTerraformProvider(), + "spacelift_user_group": resourceUserGroup(), "spacelift_vcs_agent_pool": resourceVCSAgentPool(), "spacelift_webhook": resourceWebhook(), "spacelift_named_webhook": resourceNamedWebhook(), diff --git a/spacelift/resource_user_group.go b/spacelift/resource_user_group.go new file mode 100644 index 00000000..cac7693b --- /dev/null +++ b/spacelift/resource_user_group.go @@ -0,0 +1,173 @@ +package spacelift + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/shurcooL/graphql" + + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" +) + +var validAccessLevels = []string{ + "READ", + "WRITE", + "ADMIN", +} + +func resourceUserGroup() *schema.Resource { + return &schema.Resource{ + Description: "" + + "`spacelift_user_group` represents a Spacelift **user group** - " + + "a collection of users as provided by your Identity Provider (IdP). " + + "If you assign permissions to a user group, all users in the group " + + "will have those permissions, unless the user's permissions are higher than " + + "the group's permissions.", + CreateContext: resourceUserGroupCreate, + ReadContext: resourceUserGroupRead, + UpdateContext: resourceUserGroupUpdate, + DeleteContext: resourceUserGroupDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + "access": { + + Type: schema.TypeList, + MinItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "space_id": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + "level": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(validAccessLevels, false), + }, + }, + }, + }, + }, + } +} + +func resourceUserGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + UserGroup *structs.UserGroup `graphql:"managedUserGroupCreate(input: $input)"` + } + + variables := map[string]interface{}{ + "input": structs.ManagedUserGroupCreateInput{ + Name: toString(d.Get("name")), + AccessRules: getAccessRules(d), + }, + } + + if err := meta.(*internal.Client).Mutate(ctx, "ManagedUserGroupCreate", &mutation, variables); err != nil { + return diag.Errorf("could not create user group %v: %v", toString(d.Get("name")), internal.FromSpaceliftError(err)) + } + + d.SetId(mutation.UserGroup.ID) + + return resourceUserGroupRead(ctx, d, meta) +} + +func resourceUserGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var query struct { + UserGroup *structs.UserGroup `graphql:"managedUserGroup(id: $id)"` + } + + variables := map[string]interface{}{"id": graphql.ID(d.Id())} + if err := meta.(*internal.Client).Query(ctx, "ManagedUserGroupRead", &query, variables); err != nil { + return diag.Errorf("could not query for user group: %v", err) + } + + userGroup := query.UserGroup + if userGroup == nil { + d.SetId("") + return nil + } + + d.Set("name", userGroup.Name) + + var accessList []interface{} + + for _, a := range userGroup.AccessRules { + accessList = append(accessList, map[string]interface{}{ + "space_id": a.Space, + "level": a.SpaceAccessLevel, + }) + } + + d.Set("access", accessList) + + return nil + +} + +func resourceUserGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + UserGroup *structs.UserGroup `graphql:"managedUserGroupUpdate(input: $input)"` + } + + variables := map[string]interface{}{ + "input": structs.ManagedUserGroupUpdateInput{ + ID: toID(d.Id()), + AccessRules: getAccessRules(d), + }, + } + + var ret diag.Diagnostics + + if err := meta.(*internal.Client).Mutate(ctx, "ManagedUserGroupUpdate", &mutation, variables); err != nil { + ret = append(ret, diag.Errorf("could not update user group: %v", internal.FromSpaceliftError(err))...) + } + + ret = append(ret, resourceUserGroupRead(ctx, d, meta)...) + + return ret +} + +func resourceUserGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + UserGroup *structs.UserGroup `graphql:"managedUserGroupDelete(id: $id)"` + } + + variables := map[string]interface{}{"id": toID(d.Id())} + + if err := meta.(*internal.Client).Mutate(ctx, "ManagedUserGroupDelete", &mutation, variables); err != nil { + return diag.Errorf("could not delete user group: %v", internal.FromSpaceliftError(err)) + } + + d.SetId("") + + return nil +} + +func getAccessRules(d *schema.ResourceData) []structs.SpaceAccessRuleInput { + var accessRules []structs.SpaceAccessRuleInput + for _, a := range d.Get("access").([]interface{}) { + access := a.(map[string]interface{}) + accessRules = append(accessRules, structs.SpaceAccessRuleInput{ + Space: toID(access["space_id"]), + SpaceAccessLevel: structs.SpaceAccessLevel(access["level"].(string)), + }) + } + return accessRules +}