Skip to content

Commit da5e752

Browse files
Implementing tsuru token feature (#60)
* Add support for tsuru token resource * Add acceptance tests for tsuru token * Add documentation for tsuru token * Added sensitive field for token
1 parent bed1488 commit da5e752

File tree

5 files changed

+453
-0
lines changed

5 files changed

+453
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
terraform import tsuru_token.resource_name "token_id"
2+
3+
# example
4+
terraform import tsuru_token.simple_token "my-simple-token"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
resource "tsuru_token" "simple_token" {
2+
token_id = "my-simple-token"
3+
description = "My description"
4+
team = "team-dev"
5+
expires = "24h"
6+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func Provider() *schema.Provider {
8181
"tsuru_pool": resourceTsuruPool(),
8282
"tsuru_cluster_pool": resourceTsuruClusterPool(),
8383
"tsuru_cluster": resourceTsuruCluster(),
84+
"tsuru_token": resourceTsuruToken(),
8485
},
8586
DataSourcesMap: map[string]*schema.Resource{
8687
"tsuru_app": dataSourceTsuruApp(),
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Copyright 2024 tsuru authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package provider
6+
7+
import (
8+
"context"
9+
"time"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14+
"github.com/pkg/errors"
15+
tsuru_client "github.com/tsuru/go-tsuruclient/pkg/tsuru"
16+
)
17+
18+
func resourceTsuruToken() *schema.Resource {
19+
return &schema.Resource{
20+
Description: "Tsuru Token",
21+
CreateContext: resourceTsuruTokenCreate,
22+
ReadContext: resourceTsuruTokenRead,
23+
UpdateContext: resourceTsuruTokenUpdate,
24+
DeleteContext: resourceTsuruTokenDelete,
25+
Timeouts: &schema.ResourceTimeout{
26+
Create: schema.DefaultTimeout(60 * time.Minute),
27+
Update: schema.DefaultTimeout(60 * time.Minute),
28+
Delete: schema.DefaultTimeout(60 * time.Minute),
29+
},
30+
Importer: &schema.ResourceImporter{
31+
StateContext: schema.ImportStatePassthroughContext,
32+
},
33+
Schema: map[string]*schema.Schema{
34+
"team": {
35+
Type: schema.TypeString,
36+
Description: "The team name responsible for this token",
37+
Required: true,
38+
},
39+
"token_id": {
40+
Type: schema.TypeString,
41+
Description: "Token name, must be a unique identifier, if empty it will be generated automatically",
42+
Optional: true,
43+
ForceNew: true,
44+
},
45+
"description": {
46+
Type: schema.TypeString,
47+
Description: "Token description",
48+
Optional: true,
49+
},
50+
"expires": {
51+
Type: schema.TypeString,
52+
Description: "Token expiration with suffix (s for seconds, m for minutos, h for hours, ...) 0 or unset means it never expires",
53+
Optional: true,
54+
Default: "0s",
55+
},
56+
"regenerate_on_update": {
57+
Type: schema.TypeBool,
58+
Description: "Setting regenerate will change de value of the token, invalidating the previous value",
59+
Optional: true,
60+
Default: false,
61+
},
62+
"token": {
63+
Type: schema.TypeString,
64+
Description: "Tsuru token",
65+
Computed: true,
66+
Sensitive: true,
67+
},
68+
"created_at": {
69+
Type: schema.TypeString,
70+
Description: "Token creation date",
71+
Computed: true,
72+
},
73+
"expires_at": {
74+
Type: schema.TypeString,
75+
Description: "Token expiration date",
76+
Computed: true,
77+
},
78+
"last_access": {
79+
Type: schema.TypeString,
80+
Description: "Token last access date",
81+
Computed: true,
82+
},
83+
"creator_email": {
84+
Type: schema.TypeString,
85+
Description: "Token creator email",
86+
Computed: true,
87+
},
88+
"roles": {
89+
Type: schema.TypeList,
90+
Description: "Tsuru token roles",
91+
Computed: true,
92+
Elem: &schema.Resource{
93+
Schema: map[string]*schema.Schema{
94+
"name": {
95+
Type: schema.TypeString,
96+
Computed: true,
97+
},
98+
"context_value": {
99+
Type: schema.TypeString,
100+
Computed: true,
101+
},
102+
},
103+
},
104+
},
105+
},
106+
}
107+
}
108+
109+
func resourceTsuruTokenCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
110+
provider := meta.(*tsuruProvider)
111+
112+
teamToken := tsuru_client.TeamTokenCreateArgs{
113+
Team: d.Get("team").(string),
114+
}
115+
116+
if tokenId, ok := d.GetOk("token_id"); ok {
117+
teamToken.TokenId = tokenId.(string)
118+
}
119+
120+
if desc, ok := d.GetOk("description"); ok {
121+
teamToken.Description = desc.(string)
122+
}
123+
124+
if expires, ok := d.GetOk("expires"); ok {
125+
duration, err := time.ParseDuration(expires.(string))
126+
if err != nil {
127+
return diag.FromErr(err)
128+
}
129+
teamToken.ExpiresIn = int64(duration.Seconds())
130+
}
131+
132+
err := resource.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
133+
token, _, err := provider.TsuruClient.AuthApi.TeamTokenCreate(ctx, teamToken)
134+
if err != nil {
135+
var apiError tsuru_client.GenericOpenAPIError
136+
if errors.As(err, &apiError) {
137+
if isRetryableError(apiError.Body()) {
138+
return resource.RetryableError(err)
139+
}
140+
}
141+
return resource.NonRetryableError(err)
142+
}
143+
d.SetId(token.TokenId)
144+
return nil
145+
})
146+
147+
if err != nil {
148+
return diag.FromErr(err)
149+
}
150+
151+
return resourceTsuruTokenRead(ctx, d, meta)
152+
}
153+
154+
func resourceTsuruTokenRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
155+
provider := meta.(*tsuruProvider)
156+
tokenId := d.Id()
157+
158+
teamToken, _, err := provider.TsuruClient.AuthApi.TeamTokenInfo(ctx, tokenId)
159+
if err != nil {
160+
if isNotFoundError(err) {
161+
d.SetId("")
162+
return nil
163+
}
164+
return diag.Errorf("unable to read token %s: %v", tokenId, err)
165+
}
166+
167+
d.Set("token", teamToken.Token)
168+
d.Set("created_at", formatDate(teamToken.CreatedAt))
169+
d.Set("expires_at", formatDate(teamToken.ExpiresAt))
170+
d.Set("last_access", formatDate(teamToken.LastAccess))
171+
d.Set("creator_email", teamToken.CreatorEmail)
172+
d.Set("team", teamToken.Team)
173+
d.Set("description", teamToken.Description)
174+
d.Set("roles", flattenRoles(teamToken.Roles))
175+
176+
return nil
177+
}
178+
179+
func resourceTsuruTokenUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
180+
provider := meta.(*tsuruProvider)
181+
tokenId := d.Id()
182+
183+
teamToken := tsuru_client.TeamTokenUpdateArgs{}
184+
185+
if regenerate, ok := d.GetOk("regenerate_on_update"); ok {
186+
teamToken.Regenerate = regenerate.(bool)
187+
}
188+
189+
if desc, ok := d.GetOk("description"); ok {
190+
teamToken.Description = desc.(string)
191+
}
192+
193+
if expires, ok := d.GetOk("expires"); ok {
194+
duration, err := time.ParseDuration(expires.(string))
195+
if err != nil {
196+
return diag.FromErr(err)
197+
}
198+
teamToken.ExpiresIn = int64(duration.Seconds())
199+
}
200+
201+
err := resource.RetryContext(ctx, d.Timeout(schema.TimeoutUpdate), func() *resource.RetryError {
202+
_, _, err := provider.TsuruClient.AuthApi.TeamTokenUpdate(ctx, tokenId, teamToken)
203+
if err != nil {
204+
var apiError tsuru_client.GenericOpenAPIError
205+
if errors.As(err, &apiError) {
206+
if isRetryableError(apiError.Body()) {
207+
return resource.RetryableError(err)
208+
}
209+
return resource.NonRetryableError(err)
210+
}
211+
}
212+
return nil
213+
})
214+
215+
if err != nil {
216+
return diag.Errorf("unable to update token %s: %v", tokenId, err)
217+
}
218+
219+
return resourceTsuruTokenRead(ctx, d, meta)
220+
}
221+
222+
func resourceTsuruTokenDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
223+
provider := meta.(*tsuruProvider)
224+
tokenId := d.Id()
225+
226+
err := resource.RetryContext(ctx, d.Timeout(schema.TimeoutDelete), func() *resource.RetryError {
227+
_, err := provider.TsuruClient.AuthApi.TeamTokenDelete(ctx, tokenId)
228+
if err != nil {
229+
var apiError tsuru_client.GenericOpenAPIError
230+
if errors.As(err, &apiError) {
231+
if isRetryableError(apiError.Body()) {
232+
return resource.RetryableError(err)
233+
}
234+
}
235+
return resource.NonRetryableError(err)
236+
}
237+
return nil
238+
})
239+
240+
if err != nil {
241+
return diag.Errorf("unable to delete token %s: %v", tokenId, err)
242+
}
243+
244+
return nil
245+
}
246+
247+
func flattenRoles(roles []tsuru_client.RoleInstance) []interface{} {
248+
result := []interface{}{}
249+
250+
for _, role := range roles {
251+
result = append(result, map[string]interface{}{
252+
"name": role.Name,
253+
"context_value": role.Contextvalue,
254+
})
255+
}
256+
257+
return result
258+
}
259+
260+
func formatDate(date time.Time) string {
261+
if date.IsZero() {
262+
return "-"
263+
}
264+
return date.In(time.Local).Format(time.RFC822)
265+
}

0 commit comments

Comments
 (0)