Skip to content

Commit c6da2e6

Browse files
authored
Merge pull request #903 from timofurrer/sshkey
resource/gitlab_user_sshkey: add new resource. Closes #40, 716
2 parents 91c5d96 + 1116bd9 commit c6da2e6

File tree

7 files changed

+459
-2
lines changed

7 files changed

+459
-2
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
Original file line numberDiff line numberDiff line change
@@ -12,5 +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-20220214200702-86341886e292 // indirect
1516
google.golang.org/api v0.34.0 // indirect
1617
)

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=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"strconv"
8+
"strings"
9+
"time"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
14+
gitlab "github.com/xanzy/go-gitlab"
15+
)
16+
17+
var _ = registerResource("gitlab_user_sshkey", func() *schema.Resource {
18+
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+
23+
CreateContext: resourceGitlabUserSSHKeyCreate,
24+
ReadContext: resourceGitlabUserSSHKeyRead,
25+
DeleteContext: resourceGitlabUserSSHKeyDelete,
26+
Importer: &schema.ResourceImporter{
27+
StateContext: schema.ImportStatePassthroughContext,
28+
},
29+
30+
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+
},
37+
"title": {
38+
Description: "The title of the ssh key.",
39+
Type: schema.TypeString,
40+
ForceNew: true,
41+
Required: true,
42+
},
43+
"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.",
45+
Type: schema.TypeString,
46+
ForceNew: true,
47+
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+
},
61+
},
62+
"expires_at": {
63+
Description: "The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)",
64+
Type: schema.TypeString,
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),
70+
},
71+
"key_id": {
72+
Description: "The ID of the ssh key.",
73+
Type: schema.TypeInt,
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,
80+
},
81+
},
82+
}
83+
})
84+
85+
func resourceGitlabUserSSHKeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
86+
client := meta.(*gitlab.Client)
87+
userID := d.Get("user_id").(int)
88+
89+
options := &gitlab.AddSSHKeyOptions{
90+
Title: gitlab.String(d.Get("title").(string)),
91+
Key: gitlab.String(d.Get("key").(string)),
92+
}
93+
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+
103+
key, _, err := client.Users.AddSSHKeyForUser(userID, options, gitlab.WithContext(ctx))
104+
if err != nil {
105+
return diag.FromErr(err)
106+
}
107+
108+
userIDForID := fmt.Sprintf("%d", userID)
109+
keyIDForID := fmt.Sprintf("%d", key.ID)
110+
d.SetId(buildTwoPartID(&userIDForID, &keyIDForID))
111+
return resourceGitlabUserSSHKeyRead(ctx, d, meta)
112+
}
113+
114+
func resourceGitlabUserSSHKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
115+
client := meta.(*gitlab.Client)
116+
117+
userID, keyID, err := resourceGitlabUserSSHKeyParseID(d.Id())
118+
if err != nil {
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,
125+
}
126+
127+
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+
}
133+
134+
for _, k := range keys {
135+
if k.ID == keyID {
136+
key = k
137+
break
138+
}
139+
}
140+
141+
options.Page = resp.NextPage
142+
}
143+
144+
if key == nil {
145+
log.Printf("Could not find sshkey %d for user %d", keyID, userID)
146+
d.SetId("")
147+
return nil
148+
}
149+
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))
156+
}
157+
if key.CreatedAt != nil {
158+
d.Set("created_at", key.CreatedAt.Format(time.RFC3339))
159+
}
160+
return nil
161+
}
162+
163+
func resourceGitlabUserSSHKeyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
164+
client := meta.(*gitlab.Client)
165+
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+
}
170+
171+
if _, err := client.Users.DeleteSSHKeyForUser(userID, keyID, gitlab.WithContext(ctx)); err != nil {
172+
return diag.FromErr(err)
173+
}
174+
175+
return nil
176+
}
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)