Skip to content

Commit 1116bd9

Browse files
committed
resource/gitlab_user_sshkey: add docs, refactor importer, key diffing and general functionality of the resource
1 parent c7bc0bc commit 1116bd9

File tree

7 files changed

+241
-180
lines changed

7 files changed

+241
-180
lines changed

docs/resources/user_sshkey.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "gitlab_user_sshkey Resource - terraform-provider-gitlab"
4+
subcategory: ""
5+
description: |-
6+
This resource allows to manage GitLab user SSH keys.
7+
Upstream API: GitLab API docs https://docs.gitlab.com/ee/api/users.html#single-ssh-key
8+
---
9+
10+
# gitlab_user_sshkey (Resource)
11+
12+
This resource allows to manage GitLab user SSH keys.
13+
14+
**Upstream API**: [GitLab API docs](https://docs.gitlab.com/ee/api/users.html#single-ssh-key)
15+
16+
## Example Usage
17+
18+
```terraform
19+
data "gitlab_user" "example" {
20+
username = "example-user"
21+
}
22+
23+
resource "gitlab_user_sshkey" "example" {
24+
user_id = data.gitlab_user.id
25+
title = "example-key"
26+
key = "ssh-rsa AAAA..."
27+
expires_at = "2016-01-21T00:00:00.000Z"
28+
}
29+
```
30+
31+
<!-- schema generated by tfplugindocs -->
32+
## Schema
33+
34+
### Required
35+
36+
- **key** (String) The ssh key. The SSH key `comment` (trailing part) is optional and ignored for diffing, because GitLab overrides it with the username and GitLab hostname.
37+
- **title** (String) The title of the ssh key.
38+
- **user_id** (Number) The ID of the user to add the ssh key to.
39+
40+
### Optional
41+
42+
- **expires_at** (String) The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)
43+
- **id** (String) The ID of this resource.
44+
45+
### Read-Only
46+
47+
- **created_at** (String) The time when this key was created in GitLab.
48+
- **key_id** (Number) The ID of the ssh key.
49+
50+
## Import
51+
52+
Import is supported using the following syntax:
53+
54+
```shell
55+
# You can import a user ssh key using an id made up of `{user-id}:{key}`, e.g.
56+
terraform import gitlab_user_sshkey.example 42:1
57+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# You can import a user ssh key using an id made up of `{user-id}:{key}`, e.g.
2+
terraform import gitlab_user_sshkey.example 42:1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
data "gitlab_user" "example" {
2+
username = "example-user"
3+
}
4+
5+
resource "gitlab_user_sshkey" "example" {
6+
user_id = data.gitlab_user.id
7+
title = "example-key"
8+
key = "ssh-rsa AAAA..."
9+
expires_at = "2016-01-21T00:00:00.000Z"
10+
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ require (
1212
github.com/mitchellh/hashstructure v1.1.0
1313
github.com/onsi/gomega v1.18.1
1414
github.com/xanzy/go-gitlab v0.55.1
15-
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
15+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
1616
google.golang.org/api v0.34.0 // indirect
1717
)

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
373373
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
374374
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
375375
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
376-
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
377376
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
377+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
378+
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
378379
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
379380
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
380381
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -443,8 +444,9 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
443444
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
444445
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
445446
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
446-
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
447447
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
448+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
449+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
448450
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
449451
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
450452
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

internal/provider/resource_gitlab_user_sshkey.go

+110-60
Original file line numberDiff line numberDiff line change
@@ -3,141 +3,191 @@ package provider
33
import (
44
"context"
55
"fmt"
6+
"log"
67
"strconv"
78
"strings"
9+
"time"
810

911
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1012
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1114
gitlab "github.com/xanzy/go-gitlab"
1215
)
1316

1417
var _ = registerResource("gitlab_user_sshkey", func() *schema.Resource {
1518
return &schema.Resource{
19+
Description: `This resource allows to manage GitLab user SSH keys.
20+
21+
**Upstream API**: [GitLab API docs](https://docs.gitlab.com/ee/api/users.html#single-ssh-key)`,
22+
1623
CreateContext: resourceGitlabUserSSHKeyCreate,
1724
ReadContext: resourceGitlabUserSSHKeyRead,
18-
UpdateContext: resourceGitlabUserSSHKeyUpdate,
1925
DeleteContext: resourceGitlabUserSSHKeyDelete,
2026
Importer: &schema.ResourceImporter{
21-
StateContext: resourceGitlabUserSSHKeyImporter,
27+
StateContext: schema.ImportStatePassthroughContext,
2228
},
2329

2430
Schema: map[string]*schema.Schema{
31+
"user_id": {
32+
Description: "The ID of the user to add the ssh key to.",
33+
Type: schema.TypeInt,
34+
ForceNew: true,
35+
Required: true,
36+
},
2537
"title": {
2638
Description: "The title of the ssh key.",
2739
Type: schema.TypeString,
40+
ForceNew: true,
2841
Required: true,
2942
},
3043
"key": {
31-
Description: "The ssh key.",
44+
Description: "The ssh key. The SSH key `comment` (trailing part) is optional and ignored for diffing, because GitLab overrides it with the username and GitLab hostname.",
3245
Type: schema.TypeString,
46+
ForceNew: true,
3347
Required: true,
48+
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
49+
// NOTE: the ssh keys consist of three parts: `type`, `data`, `comment`, whereas the `comment` is optional
50+
// and suppressed in the diffing. It's overridden by GitLab with the username and GitLab hostname.
51+
newParts := strings.SplitN(new, " ", 3)
52+
oldParts := strings.SplitN(old, " ", 3)
53+
if len(newParts) < 2 || len(oldParts) < 2 {
54+
// NOTE: at least one of the keys doesn't have the required two parts, thus we just compare them
55+
return new == old
56+
}
57+
58+
// NOTE: both keys have the required two parts, thus we compare the parts separately, ignoring the rest
59+
return newParts[0] == oldParts[0] && newParts[1] == oldParts[1]
60+
},
3461
},
35-
"created_at": {
36-
Description: "Create time.",
62+
"expires_at": {
63+
Description: "The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)",
3764
Type: schema.TypeString,
38-
Computed: true,
65+
ForceNew: true,
66+
Optional: true,
67+
// NOTE: since RFC3339 is pretty much a subset of ISO8601 and actually expected by GitLab,
68+
// we use it here to avoid having to parse the string ourselves.
69+
ValidateDiagFunc: validation.ToDiagFunc(validation.IsRFC3339Time),
3970
},
40-
"user_id": {
41-
Description: "The id of the user to add the ssh key to.",
71+
"key_id": {
72+
Description: "The ID of the ssh key.",
4273
Type: schema.TypeInt,
43-
ForceNew: true,
44-
Required: true,
74+
Computed: true,
75+
},
76+
"created_at": {
77+
Description: "The time when this key was created in GitLab.",
78+
Type: schema.TypeString,
79+
Computed: true,
4580
},
4681
},
4782
}
4883
})
4984

50-
func resourceGitlabUserSSHKeyImporter(ctx context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
51-
s := strings.Split(d.Id(), ":")
52-
if len(s) != 2 {
53-
d.SetId("")
54-
return nil, fmt.Errorf("Invalid SSH Key import format; expected '{user_id}:{key_id}'")
55-
}
56-
57-
userID, err := strconv.Atoi(s[0])
58-
if err != nil {
59-
return nil, fmt.Errorf("Invalid SSH Key import format; expected '{user_id}:{key_id}'")
60-
}
61-
62-
d.Set("user_id", userID)
63-
d.SetId(s[1])
64-
65-
return []*schema.ResourceData{d}, nil
66-
}
67-
68-
func resourceGitlabUserSSHKeySetToState(d *schema.ResourceData, key *gitlab.SSHKey) {
69-
d.Set("title", key.Title)
70-
d.Set("key", key.Key)
71-
d.Set("created_at", key.CreatedAt.String())
72-
}
73-
7485
func resourceGitlabUserSSHKeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
7586
client := meta.(*gitlab.Client)
76-
7787
userID := d.Get("user_id").(int)
7888

7989
options := &gitlab.AddSSHKeyOptions{
8090
Title: gitlab.String(d.Get("title").(string)),
8191
Key: gitlab.String(d.Get("key").(string)),
8292
}
8393

94+
if expiresAt, ok := d.GetOk("expires_at"); ok {
95+
parsedExpiresAt, err := time.Parse(time.RFC3339, expiresAt.(string))
96+
if err != nil {
97+
return diag.Errorf("failed to parse created_at: %s. It must be in valid RFC3339 format.", err)
98+
}
99+
gitlabExpiresAt := gitlab.ISOTime(parsedExpiresAt)
100+
options.ExpiresAt = &gitlabExpiresAt
101+
}
102+
84103
key, _, err := client.Users.AddSSHKeyForUser(userID, options, gitlab.WithContext(ctx))
85104
if err != nil {
86105
return diag.FromErr(err)
87106
}
88107

89-
d.SetId(fmt.Sprintf("%d", key.ID))
90-
108+
userIDForID := fmt.Sprintf("%d", userID)
109+
keyIDForID := fmt.Sprintf("%d", key.ID)
110+
d.SetId(buildTwoPartID(&userIDForID, &keyIDForID))
91111
return resourceGitlabUserSSHKeyRead(ctx, d, meta)
92112
}
93113

94114
func resourceGitlabUserSSHKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
95115
client := meta.(*gitlab.Client)
96116

97-
id, _ := strconv.Atoi(d.Id())
98-
userID := d.Get("user_id").(int)
99-
100-
keys, _, err := client.Users.ListSSHKeysForUser(userID, &gitlab.ListSSHKeysForUserOptions{}, gitlab.WithContext(ctx))
117+
userID, keyID, err := resourceGitlabUserSSHKeyParseID(d.Id())
101118
if err != nil {
102-
return diag.FromErr(err)
119+
return diag.Errorf("unable to parse user ssh key resource id: %s: %v", d.Id(), err)
120+
}
121+
122+
options := &gitlab.ListSSHKeysForUserOptions{
123+
Page: 1,
124+
PerPage: 20,
103125
}
104126

105127
var key *gitlab.SSHKey
128+
for options.Page != 0 && key == nil {
129+
keys, resp, err := client.Users.ListSSHKeysForUser(userID, options, gitlab.WithContext(ctx))
130+
if err != nil {
131+
return diag.FromErr(err)
132+
}
106133

107-
for _, k := range keys {
108-
if k.ID == id {
109-
key = k
110-
break
134+
for _, k := range keys {
135+
if k.ID == keyID {
136+
key = k
137+
break
138+
}
111139
}
140+
141+
options.Page = resp.NextPage
112142
}
113143

114144
if key == nil {
115-
return diag.Errorf("Could not find sshkey %d for user %d", id, userID)
145+
log.Printf("Could not find sshkey %d for user %d", keyID, userID)
146+
d.SetId("")
147+
return nil
116148
}
117149

118-
resourceGitlabUserSSHKeySetToState(d, key)
119-
return nil
120-
}
121-
122-
func resourceGitlabUserSSHKeyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
123-
if d := resourceGitlabUserSSHKeyDelete(ctx, d, meta); d.HasError() {
124-
return d
150+
d.Set("user_id", userID)
151+
d.Set("key_id", keyID)
152+
d.Set("title", key.Title)
153+
d.Set("key", key.Key)
154+
if key.ExpiresAt != nil {
155+
d.Set("expires_at", key.ExpiresAt.Format(time.RFC3339))
125156
}
126-
if d := resourceGitlabUserSSHKeyCreate(ctx, d, meta); d.HasError() {
127-
return d
157+
if key.CreatedAt != nil {
158+
d.Set("created_at", key.CreatedAt.Format(time.RFC3339))
128159
}
129-
return resourceGitlabUserSSHKeyRead(ctx, d, meta)
160+
return nil
130161
}
131162

132163
func resourceGitlabUserSSHKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
133164
client := meta.(*gitlab.Client)
134165

135-
id, _ := strconv.Atoi(d.Id())
136-
userID := d.Get("user_id").(int)
166+
userID, keyID, err := resourceGitlabUserSSHKeyParseID(d.Id())
167+
if err != nil {
168+
return diag.Errorf("unable to parse user ssh key resource id: %s: %v", d.Id(), err)
169+
}
137170

138-
if _, err := client.Users.DeleteSSHKeyForUser(userID, id, gitlab.WithContext(ctx)); err != nil {
171+
if _, err := client.Users.DeleteSSHKeyForUser(userID, keyID, gitlab.WithContext(ctx)); err != nil {
139172
return diag.FromErr(err)
140173
}
141174

142175
return nil
143176
}
177+
178+
func resourceGitlabUserSSHKeyParseID(id string) (int, int, error) {
179+
userIDFromID, keyIDFromID, err := parseTwoPartID(id)
180+
if err != nil {
181+
return 0, 0, err
182+
}
183+
userID, err := strconv.Atoi(userIDFromID)
184+
if err != nil {
185+
return 0, 0, err
186+
}
187+
keyID, err := strconv.Atoi(keyIDFromID)
188+
if err != nil {
189+
return 0, 0, err
190+
}
191+
192+
return userID, keyID, nil
193+
}

0 commit comments

Comments
 (0)