From c11e379f024426e261c1e1e6371732b91fdbcd2c Mon Sep 17 00:00:00 2001 From: Srevin Saju Date: Tue, 28 May 2024 21:58:40 +0300 Subject: [PATCH] feat: add service_account_token resource * adds a new resource `doppler_service_account_token` * added example usage * autogenerated docs for newly added resource --- docs/resources/service_account_token.md | 41 ++++++ doppler/api.go | 44 ++++++ doppler/models.go | 16 +++ doppler/provider.go | 3 +- doppler/resource_service_account_token.go | 129 ++++++++++++++++++ examples/resources/service_account_token.tf | 7 + .../resources/service_account_token.md.tmpl | 16 +++ 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 docs/resources/service_account_token.md create mode 100644 doppler/resource_service_account_token.go create mode 100644 examples/resources/service_account_token.tf create mode 100644 templates/resources/service_account_token.md.tmpl diff --git a/docs/resources/service_account_token.md b/docs/resources/service_account_token.md new file mode 100644 index 0000000..23f7582 --- /dev/null +++ b/docs/resources/service_account_token.md @@ -0,0 +1,41 @@ +--- +page_title: "doppler_service_account_token Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage a Doppler service_account_token. +--- + +# doppler_service_account_token (Resource) + +Manage a Doppler service account token. + +## Example Usage + +```terraform +resource "doppler_service_account_token" "builder_ci_token" { + service_account = "builder" + name = "Builder CI Token" + expires_at = "2024-05-30T11:00:00.000Z" +} + +# Service token key available as `doppler_service_account_token.builder_ci_token.api_key` +``` + + +## Schema + +### Required + +- `name` (String) The display name of the API token +- `service_account_slug` (String) Slug of the service account + +### Optional + +- `expires_at` (String) The datetime at which the API token should expire. If not provided, the API token will remain valid indefinitely unless manually revoked + +### Read-Only + +- `api_key` (String, Sensitive) The api key used to authenticate the service account +- `created_at` (String) The datetime that the token was created. +- `id` (String) The ID of this resource. +- `slug` (String) Slug of the service account token diff --git a/doppler/api.go b/doppler/api.go index 5535422..e40bd6b 100644 --- a/doppler/api.go +++ b/doppler/api.go @@ -835,6 +835,50 @@ func (client APIClient) DeleteServiceAccount(ctx context.Context, slug string) e return nil } +// Service Account Tokens + +func (client APIClient) GetServiceAccountToken(ctx context.Context, serviceAccountSlug string, slug string) (ServiceAccountTokenResponse, error) { + response, err := client.PerformRequestWithRetry(ctx, "GET", fmt.Sprintf("/v3/workplace/service_accounts/service_account/%s/tokens/token/%s", url.QueryEscape(serviceAccountSlug), url.QueryEscape(slug)), []QueryParam{}, nil) + if err != nil { + return ServiceAccountTokenResponse{}, err + } + var result ServiceAccountTokenResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return ServiceAccountTokenResponse{}, &APIError{Err: err, Message: "Unable to parse service account tokens"} + } + return result, nil +} + +func (client APIClient) CreateServiceAccountToken(ctx context.Context, serviceAccountSlug string, name string, expiresAt string) (*ServiceAccountTokenResponse, error) { + payload := map[string]interface{}{ + "name": name, + } + if expiresAt != "" { + payload["expires_at"] = expiresAt + } + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize account service token"} + } + response, err := client.PerformRequestWithRetry(ctx, "POST", fmt.Sprintf("/v3/workplace/service_accounts/service_account/%s/tokens", url.QueryEscape(serviceAccountSlug)), []QueryParam{}, body) + if err != nil { + return nil, err + } + var result ServiceAccountTokenResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse service account token"} + } + return &result, nil +} + +func (client APIClient) DeleteServiceAccountToken(ctx context.Context, serviceAccountSlug string, slug string) error { + _, err := client.PerformRequestWithRetry(ctx, "DELETE", fmt.Sprintf("/v3/workplace/service_accounts/service_account/%s/tokens/token/%s", url.QueryEscape(serviceAccountSlug), url.QueryEscape(slug)), []QueryParam{}, nil) + if err != nil { + return err + } + return nil +} + // Groups func (client APIClient) GetGroup(ctx context.Context, slug string) (*Group, error) { diff --git a/doppler/models.go b/doppler/models.go index 8190c2a..a6fe7f4 100644 --- a/doppler/models.go +++ b/doppler/models.go @@ -208,6 +208,22 @@ func parseServiceTokenResourceId(id string) (project string, config string, slug return tokens[0], tokens[1], tokens[2], nil } +type ServiceAccountToken struct { + Name string `json:"name"` + ExpiresAt string `json:"expires_at"` + CreatedAt string `json:"created_at"` + Slug string `json:"slug"` +} + +type ServiceAccountTokenResponse struct { + ServiceAccountToken ServiceAccountToken `json:"api_token"` + ApiKey string `json:"api_key"` +} + +func (t ServiceAccountToken) getResourceId() string { + return t.Slug +} + type WorkplaceRole struct { Name string `json:"name"` Permissions []string `json:"permissions"` diff --git a/doppler/provider.go b/doppler/provider.go index 6a09518..4a5b6e1 100644 --- a/doppler/provider.go +++ b/doppler/provider.go @@ -38,7 +38,8 @@ func Provider() *schema.Provider { "doppler_config": resourceConfig(), "doppler_service_token": resourceServiceToken(), - "doppler_service_account": resourceServiceAccount(), + "doppler_service_account": resourceServiceAccount(), + "doppler_service_account_token": resourceServiceAccountToken(), "doppler_group": resourceGroup(), "doppler_group_member": resourceGroupMemberWorkplaceUser(), diff --git a/doppler/resource_service_account_token.go b/doppler/resource_service_account_token.go new file mode 100644 index 0000000..c05a9d6 --- /dev/null +++ b/doppler/resource_service_account_token.go @@ -0,0 +1,129 @@ +package doppler + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceServiceAccountToken() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceServiceAccountTokenCreate, + ReadContext: resourceServiceAccountTokenRead, + DeleteContext: resourceServiceAccountTokenDelete, + // ForceNew is specified for all user-specified fields + // Service account tokens cannot be moved, renamed, or edited to change their access + Schema: map[string]*schema.Schema{ + "service_account_slug": { + Description: "Slug of the service account", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Description: "The display name of the API token", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "slug": { + Description: "Slug of the service account token", + Type: schema.TypeString, + Computed: true, + }, + "expires_at": { + Description: "The datetime at which the API token should expire. " + + "If not provided, the API token will remain valid indefinitely unless manually revoked", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "created_at": { + Description: "The datetime that the token was created.", + Type: schema.TypeString, + Computed: true, + }, + "api_key": { + Description: "The api key used to authenticate the service account", + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func resourceServiceAccountTokenCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + serviceAccount := d.Get("service_account_slug").(string) + name := d.Get("name").(string) + expiresAt, ok := d.Get("expires_at").(string) + if !ok { + expiresAt = "" + } + + token, err := client.CreateServiceAccountToken(ctx, serviceAccount, name, expiresAt) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(token.ServiceAccountToken.getResourceId()) + + if err = d.Set("expires_at", token.ServiceAccountToken.ExpiresAt); err != nil { + return diag.FromErr(err) + } + if err = d.Set("created_at", token.ServiceAccountToken.CreatedAt); err != nil { + return diag.FromErr(err) + } + if err = d.Set("api_key", token.ApiKey); err != nil { + return diag.FromErr(err) + } + if err = d.Set("slug", token.ServiceAccountToken.Slug); err != nil { + return diag.FromErr(err) + } + + return diags +} + +func resourceServiceAccountTokenRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + serviceAccount := d.Get("service_account_slug").(string) + slug := d.Id() + + token, err := client.GetServiceAccountToken(ctx, serviceAccount, slug) + if err != nil { + return diag.FromErr(err) + } + + if err = d.Set("name", token.ServiceAccountToken.Name); err != nil { + return diag.FromErr(err) + } + if err = d.Set("expires_at", token.ServiceAccountToken.ExpiresAt); err != nil { + return diag.FromErr(err) + } + if err = d.Set("created_at", token.ServiceAccountToken.CreatedAt); err != nil { + return diag.FromErr(err) + } + if err = d.Set("slug", token.ServiceAccountToken.Slug); err != nil { + return diag.FromErr(err) + } + return diags +} + +func resourceServiceAccountTokenDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + serviceAccount := d.Get("service_account_slug").(string) + slug := d.Id() + + if err := client.DeleteServiceAccountToken(ctx, serviceAccount, slug); err != nil { + return diag.FromErr(err) + } + return diags +} diff --git a/examples/resources/service_account_token.tf b/examples/resources/service_account_token.tf new file mode 100644 index 0000000..9652403 --- /dev/null +++ b/examples/resources/service_account_token.tf @@ -0,0 +1,7 @@ +resource "doppler_service_account_token" "builder_ci_token" { + service_account = "builder" + name = "Builder CI Token" + expires_at = "2024-05-30T11:00:00.000Z" +} + +# Service token key available as `doppler_service_account_token.builder_ci_token.api_key` diff --git a/templates/resources/service_account_token.md.tmpl b/templates/resources/service_account_token.md.tmpl new file mode 100644 index 0000000..6a61039 --- /dev/null +++ b/templates/resources/service_account_token.md.tmpl @@ -0,0 +1,16 @@ +--- +page_title: "doppler_service_account_token Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage a Doppler service_account_token. +--- + +# doppler_service_account_token (Resource) + +Manage a Doppler service account token. + +## Example Usage + +{{tffile "examples/resources/service_account_token.tf"}} + +{{ .SchemaMarkdown | trimspace }}