diff --git a/docs/resources/project_role.md b/docs/resources/project_role.md new file mode 100644 index 0000000..3983181 --- /dev/null +++ b/docs/resources/project_role.md @@ -0,0 +1,41 @@ +--- +page_title: "doppler_project_role Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage a Doppler project role. +--- + +# doppler_project_role (Resource) + +Manage a Doppler project_role. + +## Example Usage + +```terraform +resource "doppler_project_role" "log_viewer" { + name = "Log Viewer" + permissions = ["enclave_config_logs"] +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the Doppler project role +- `permissions` (Set of String) A list of [Doppler project permissions](https://docs.doppler.com/reference/project_roles-create) + +### Read-Only + +- `id` (String) The ID of this resource. +- `identifier` (String) The role's unique identifier +- `is_custom_role` (Boolean) Whether or not the role is custom (as opposed to Doppler built-in) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import doppler_project_role.default +``` diff --git a/doppler/api.go b/doppler/api.go index e40bd6b..7aec820 100644 --- a/doppler/api.go +++ b/doppler/api.go @@ -337,8 +337,69 @@ func (client APIClient) DeleteProject(ctx context.Context, name string) error { return nil } -// Project Members +// Project Roles + +func (client APIClient) CreateProjectRole(ctx context.Context, name string, permissions []string) (*ProjectRole, error) { + payload := map[string]interface{}{ + "name": name, + "permissions": permissions, + } + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize project role"} + } + response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/projects/roles", []QueryParam{}, body) + if err != nil { + return nil, err + } + var result CreateProjectRoleResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project role"} + } + return &result.Role, nil +} + +func (client APIClient) GetProjectRole(ctx context.Context, identifier string) (*ProjectRole, error) { + response, err := client.PerformRequestWithRetry(ctx, "GET", fmt.Sprintf("/v3/projects/roles/role/%s", url.PathEscape(identifier)), []QueryParam{}, nil) + if err != nil { + return nil, err + } + var result UpdateProjectRoleResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project role"} + } + return &result.Role, nil +} +func (client APIClient) UpdateProjectRole(ctx context.Context, identifier string, name string, permissions []string) (*ProjectRole, error) { + payload := map[string]interface{}{ + "name": name, + "permissions": permissions, + } + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize project role"} + } + response, err := client.PerformRequestWithRetry(ctx, "PATCH", fmt.Sprintf("/v3/projects/roles/role/%s", url.PathEscape(identifier)), []QueryParam{}, body) + if err != nil { + return nil, err + } + var result UpdateProjectRoleResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project role"} + } + return &result.Role, nil +} + +func (client APIClient) DeleteProjectRole(ctx context.Context, identifier string) error { + _, err := client.PerformRequestWithRetry(ctx, "DELETE", fmt.Sprintf("/v3/projects/roles/role/%s", url.PathEscape(identifier)), []QueryParam{}, nil) + if err != nil { + return err + } + return nil +} + +// Project Members func (client APIClient) CreateProjectMember(ctx context.Context, project string, memberType string, memberSlug string, role string, environments []string) (*ProjectMember, error) { payload := map[string]interface{}{ "project": project, diff --git a/doppler/models.go b/doppler/models.go index a6fe7f4..972da6b 100644 --- a/doppler/models.go +++ b/doppler/models.go @@ -248,6 +248,26 @@ type SimpleProjectRole struct { Identifier string `json:"identifier"` } +type ProjectRole struct { + Identifier string `json:"identifier"` + Name string `json:"name"` + Permissions []string `json:"permissions"` + CreatedAt string `json:"created_at"` + IsCustomRole bool `json:"is_custom_role"` +} + +type GetProjectRoleResponse struct { + Role ProjectRole `json:"role"` +} + +type CreateProjectRoleResponse struct { + Role ProjectRole `json:"role"` +} + +type UpdateProjectRoleResponse struct { + Role ProjectRole `json:"role"` +} + type Group struct { Slug string `json:"slug"` Name string `json:"name"` diff --git a/doppler/provider.go b/doppler/provider.go index 4a5b6e1..cb55373 100644 --- a/doppler/provider.go +++ b/doppler/provider.go @@ -38,6 +38,8 @@ func Provider() *schema.Provider { "doppler_config": resourceConfig(), "doppler_service_token": resourceServiceToken(), + "doppler_project_role": resourceProjectRole(), + "doppler_service_account": resourceServiceAccount(), "doppler_service_account_token": resourceServiceAccountToken(), diff --git a/doppler/resource_project_role.go b/doppler/resource_project_role.go new file mode 100644 index 0000000..d89c7c5 --- /dev/null +++ b/doppler/resource_project_role.go @@ -0,0 +1,134 @@ +package doppler + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceProjectRole() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceProjectRoleCreate, + ReadContext: resourceProjectRoleRead, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + UpdateContext: resourceProjectRoleUpdate, + DeleteContext: resourceProjectRoleDelete, + Schema: map[string]*schema.Schema{ + "name": { + Description: "The name of the Doppler project role", + Type: schema.TypeString, + Required: true, + }, + "permissions": { + Description: "A list of [Doppler project permissions](https://docs.doppler.com/reference/project_roles-create)", + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + }, + "identifier": { + Description: "The role's unique identifier", + Type: schema.TypeString, + Computed: true, + }, + "is_custom_role": { + Description: "Whether or not the role is custom (as opposed to Doppler built-in)", + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func updateProjectRoleData(d *schema.ResourceData, role *ProjectRole) diag.Diagnostics { + d.SetId(role.Identifier) + + if err := d.Set("name", role.Name); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("permissions", role.Permissions); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("identifier", role.Identifier); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("is_custom_role", role.IsCustomRole); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceProjectRoleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + name := d.Get("name").(string) + permissions := []string{} + for _, v := range d.Get("permissions").(*schema.Set).List() { + permissions = append(permissions, v.(string)) + } + + role, err := client.CreateProjectRole(ctx, name, permissions) + if err != nil { + return diag.FromErr(err) + } + + diags = append(diags, updateProjectRoleData(d, role)...) + return diags +} + +func resourceProjectRoleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + identifier := d.Id() + newName := d.Get("name").(string) + newPermissions := []string{} + for _, v := range d.Get("permissions").(*schema.Set).List() { + newPermissions = append(newPermissions, v.(string)) + } + + role, err := client.UpdateProjectRole(ctx, identifier, newName, newPermissions) + if err != nil { + return diag.FromErr(err) + } + diags = append(diags, updateProjectRoleData(d, role)...) + return diags +} + +func resourceProjectRoleRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + + identifier := d.Id() + + role, err := client.GetProjectRole(ctx, identifier) + if err != nil { + return handleNotFoundError(err, d) + } + + diags = append(diags, updateProjectRoleData(d, role)...) + return diags +} + +func resourceProjectRoleDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + + identifier := d.Id() + if err := client.DeleteProjectRole(ctx, identifier); err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/examples/resources/project_role.tf b/examples/resources/project_role.tf new file mode 100644 index 0000000..56aaaf6 --- /dev/null +++ b/examples/resources/project_role.tf @@ -0,0 +1,4 @@ +resource "doppler_project_role" "log_viewer" { + name = "Log Viewer" + permissions = ["enclave_config_logs"] +} diff --git a/templates/resources/project_role.md.tmpl b/templates/resources/project_role.md.tmpl new file mode 100644 index 0000000..252f84d --- /dev/null +++ b/templates/resources/project_role.md.tmpl @@ -0,0 +1,24 @@ +--- +page_title: "doppler_project_role Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage a Doppler project role. +--- + +# doppler_project_role (Resource) + +Manage a Doppler project_role. + +## Example Usage + +{{tffile "examples/resources/project_role.tf"}} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +Import is supported using the following syntax: + +```shell +terraform import doppler_project_role.default +```