Skip to content

Commit 743fa2e

Browse files
committed
resource/gitlab_topic: Support avatars
1 parent 13f1c1a commit 743fa2e

File tree

7 files changed

+183
-12
lines changed

7 files changed

+183
-12
lines changed

docs/resources/topic.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The `gitlab_topic` resource allows to manage the lifecycle of topics that are th
2525
resource "gitlab_topic" "functional_programming" {
2626
name = "Functional Programming"
2727
description = "In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions."
28+
avatar = "${path.module}/avatar.png"
2829
}
2930
```
3031

@@ -33,13 +34,19 @@ resource "gitlab_topic" "functional_programming" {
3334

3435
### Required
3536

36-
- `name` (String) The topic's name
37+
- `name` (String) The topic's name.
3738

3839
### Optional
3940

40-
- `description` (String) A text describing the topic
41+
- `avatar` (String) A local path to the avatar image to upload. **Note**: not available for imported resources
42+
- `description` (String) A text describing the topic.
4143
- `id` (String) The ID of this resource.
42-
- `soft_destroy` (Boolean, Deprecated) Empty the topics fields instead of deleting it
44+
- `soft_destroy` (Boolean, Deprecated) Empty the topics fields instead of deleting it.
45+
46+
### Read-Only
47+
48+
- `_avatar_hash` (String) The hash of the avatar image. **Note**: this is an internally used attribute to track the avatar image.
49+
- `avatar_url` (String) The URL of the avatar image.
4350

4451
## Import
4552

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
resource "gitlab_topic" "functional_programming" {
22
name = "Functional Programming"
33
description = "In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions."
4+
avatar = "${path.module}/avatar.png"
45
}

internal/provider/helper_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package provider
22

33
import (
44
"fmt"
5+
"io"
56
"os"
67
"strings"
78
"testing"
@@ -394,3 +395,23 @@ func testCheckResourceAttrLazy(name string, key string, value func() string) res
394395
return resource.TestCheckResourceAttr(name, key, value())(s)
395396
}
396397
}
398+
399+
func copyFile(src, dst string) error {
400+
in, err := os.Open(src)
401+
if err != nil {
402+
return err
403+
}
404+
defer in.Close()
405+
406+
out, err := os.Create(dst)
407+
if err != nil {
408+
return err
409+
}
410+
defer out.Close()
411+
412+
_, err = io.Copy(out, in)
413+
if err != nil {
414+
return err
415+
}
416+
return out.Close()
417+
}

internal/provider/resource_gitlab_topic.go

+105-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package provider
22

33
import (
4+
"bytes"
45
"context"
6+
"crypto/sha256"
57
"fmt"
8+
"io"
69
"log"
10+
"os"
711
"strconv"
812

913
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -32,21 +36,57 @@ var _ = registerResource("gitlab_topic", func() *schema.Resource {
3236

3337
Schema: map[string]*schema.Schema{
3438
"name": {
35-
Description: "The topic's name",
39+
Description: "The topic's name.",
3640
Type: schema.TypeString,
3741
Required: true,
3842
},
3943
"soft_destroy": {
40-
Description: "Empty the topics fields instead of deleting it",
44+
Description: "Empty the topics fields instead of deleting it.",
4145
Type: schema.TypeBool,
4246
Optional: true,
4347
Deprecated: "GitLab 14.9 introduced the proper deletion of topics. This field is no longer needed.",
4448
},
4549
"description": {
46-
Description: "A text describing the topic",
50+
Description: "A text describing the topic.",
4751
Type: schema.TypeString,
4852
Optional: true,
4953
},
54+
"avatar": {
55+
Description: "A local path to the avatar image to upload. **Note**: not available for imported resources",
56+
Type: schema.TypeString,
57+
Optional: true,
58+
},
59+
"_avatar_hash": {
60+
Description: "The hash of the avatar image. **Note**: this is an internally used attribute to track the avatar image.",
61+
Type: schema.TypeString,
62+
Computed: true,
63+
},
64+
"avatar_url": {
65+
Description: "The URL of the avatar image.",
66+
Type: schema.TypeString,
67+
Computed: true,
68+
},
69+
},
70+
CustomizeDiff: func(ctx context.Context, rd *schema.ResourceDiff, i interface{}) error {
71+
if v, ok := rd.GetOk("avatar"); ok {
72+
avatarPath := v.(string)
73+
avatarFile, err := os.Open(avatarPath)
74+
if err != nil {
75+
return fmt.Errorf("Unable to open avatar file %s: %s", avatarPath, err)
76+
}
77+
78+
avatarHash, err := getSha256(avatarFile)
79+
if err != nil {
80+
return fmt.Errorf("Unable to get hash from avatar file %s: %s", avatarPath, err)
81+
}
82+
83+
if rd.Get("_avatar_hash").(string) != avatarHash {
84+
if err := rd.SetNew("_avatar_hash", avatarHash); err != nil {
85+
return fmt.Errorf("Unable to set _avatar_hash: %s", err)
86+
}
87+
}
88+
}
89+
return nil
5090
},
5191
}
5292
})
@@ -61,6 +101,16 @@ func resourceGitlabTopicCreate(ctx context.Context, d *schema.ResourceData, meta
61101
options.Description = gitlab.String(v.(string))
62102
}
63103

104+
avatarHash := ""
105+
if v, ok := d.GetOk("avatar"); ok {
106+
avatar, hash, err := resourceGitlabTopicGetAvatar(v.(string))
107+
if err != nil {
108+
return diag.FromErr(err)
109+
}
110+
options.Avatar = avatar
111+
avatarHash = hash
112+
}
113+
64114
log.Printf("[DEBUG] create gitlab topic %s", *options.Name)
65115

66116
topic, _, err := client.Topics.CreateTopic(options, gitlab.WithContext(ctx))
@@ -69,7 +119,9 @@ func resourceGitlabTopicCreate(ctx context.Context, d *schema.ResourceData, meta
69119
}
70120

71121
d.SetId(fmt.Sprintf("%d", topic.ID))
72-
122+
if options.Avatar != nil {
123+
d.Set("_avatar_hash", avatarHash)
124+
}
73125
return resourceGitlabTopicRead(ctx, d, meta)
74126
}
75127

@@ -95,6 +147,7 @@ func resourceGitlabTopicRead(ctx context.Context, d *schema.ResourceData, meta i
95147
d.SetId(fmt.Sprintf("%d", topic.ID))
96148
d.Set("name", topic.Name)
97149
d.Set("description", topic.Description)
150+
d.Set("avatar_url", topic.AvatarURL)
98151

99152
return nil
100153
}
@@ -111,6 +164,26 @@ func resourceGitlabTopicUpdate(ctx context.Context, d *schema.ResourceData, meta
111164
options.Description = gitlab.String(d.Get("description").(string))
112165
}
113166

167+
if d.HasChanges("avatar", "_avatar_hash") {
168+
avatarPath := d.Get("avatar").(string)
169+
var avatar *gitlab.TopicAvatar
170+
var avatarHash string
171+
// NOTE: the avatar should be removed
172+
if avatarPath == "" {
173+
avatar = &gitlab.TopicAvatar{}
174+
avatarHash = ""
175+
} else {
176+
changedAvatar, changedAvatarHash, err := resourceGitlabTopicGetAvatar(avatarPath)
177+
if err != nil {
178+
return diag.FromErr(err)
179+
}
180+
avatar = changedAvatar
181+
avatarHash = changedAvatarHash
182+
}
183+
options.Avatar = avatar
184+
d.Set("_avatar_hash", avatarHash)
185+
}
186+
114187
log.Printf("[DEBUG] update gitlab topic %s", d.Id())
115188

116189
topicID, err := strconv.Atoi(d.Id())
@@ -121,7 +194,6 @@ func resourceGitlabTopicUpdate(ctx context.Context, d *schema.ResourceData, meta
121194
if err != nil {
122195
return diag.Errorf("Failed to update topic %d: %s", topicID, err)
123196
}
124-
125197
return resourceGitlabTopicRead(ctx, d, meta)
126198
}
127199

@@ -166,3 +238,31 @@ func resourceGitlabTopicDelete(ctx context.Context, d *schema.ResourceData, meta
166238

167239
return nil
168240
}
241+
242+
func resourceGitlabTopicGetAvatar(avatarPath string) (*gitlab.TopicAvatar, string, error) {
243+
avatarFile, err := os.Open(avatarPath)
244+
if err != nil {
245+
return nil, "", fmt.Errorf("Unable to open avatar file %s: %s", avatarPath, err)
246+
}
247+
248+
avatarImageBuf := &bytes.Buffer{}
249+
teeReader := io.TeeReader(avatarFile, avatarImageBuf)
250+
avatarHash, err := getSha256(teeReader)
251+
if err != nil {
252+
return nil, "", fmt.Errorf("Unable to get hash from avatar file %s: %s", avatarPath, err)
253+
}
254+
255+
avatarImageReader := bytes.NewReader(avatarImageBuf.Bytes())
256+
return &gitlab.TopicAvatar{
257+
Filename: avatarPath,
258+
Image: avatarImageReader,
259+
}, avatarHash, nil
260+
}
261+
262+
func getSha256(r io.Reader) (string, error) {
263+
h := sha256.New()
264+
if _, err := io.Copy(h, r); err != nil {
265+
return "", fmt.Errorf("Unable to get hash %s", err)
266+
}
267+
return fmt.Sprintf("%x", h.Sum(nil)), nil
268+
}

internal/provider/resource_gitlab_topic_test.go

+46-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package provider
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
67
"testing"
78

@@ -36,7 +37,7 @@ func TestAccGitlabTopic_basic(t *testing.T) {
3637
ImportState: true,
3738
ImportStateVerify: true,
3839
ImportStateVerifyIgnore: []string{
39-
"soft_destroy",
40+
"avatar", "_avatar_hash", "soft_destroy",
4041
},
4142
},
4243
// Update the topics values
@@ -48,6 +49,8 @@ func TestAccGitlabTopic_basic(t *testing.T) {
4849
Name: fmt.Sprintf("foo-full-%d", rInt),
4950
Description: "Terraform acceptance tests",
5051
}),
52+
resource.TestCheckResourceAttrSet("gitlab_topic.foo", "avatar_url"),
53+
resource.TestCheckResourceAttr("gitlab_topic.foo", "_avatar_hash", "8d29d9c393facb9d86314eb347a03fde503f2c0422bf55af7df086deb126107e"),
5154
),
5255
},
5356
// Verify import
@@ -56,7 +59,43 @@ func TestAccGitlabTopic_basic(t *testing.T) {
5659
ImportState: true,
5760
ImportStateVerify: true,
5861
ImportStateVerifyIgnore: []string{
59-
"soft_destroy",
62+
"avatar", "_avatar_hash", "soft_destroy",
63+
},
64+
},
65+
// Update the avatar image, but keep the filename to test the `CustomizeDiff` function
66+
{
67+
Config: testAccGitlabTopicFullConfig(rInt),
68+
PreConfig: func() {
69+
// overwrite the avatar image file
70+
if err := copyFile("testdata/gitlab_topic/avatar.png", "testdata/gitlab_topic/avatar.png.bak"); err != nil {
71+
t.Fatalf("failed to backup the avatar image file: %v", err)
72+
}
73+
if err := copyFile("testdata/gitlab_topic/avatar-update.png", "testdata/gitlab_topic/avatar.png"); err != nil {
74+
t.Fatalf("failed to overwrite the avatar image file: %v", err)
75+
}
76+
t.Cleanup(func() {
77+
if err := os.Rename("testdata/gitlab_topic/avatar.png.bak", "testdata/gitlab_topic/avatar.png"); err != nil {
78+
t.Fatalf("failed to restore the avatar image file: %v", err)
79+
}
80+
})
81+
},
82+
Check: resource.ComposeTestCheckFunc(
83+
testAccCheckGitlabTopicExists("gitlab_topic.foo", &topic),
84+
testAccCheckGitlabTopicAttributes(&topic, &testAccGitlabTopicExpectedAttributes{
85+
Name: fmt.Sprintf("foo-full-%d", rInt),
86+
Description: "Terraform acceptance tests",
87+
}),
88+
resource.TestCheckResourceAttrSet("gitlab_topic.foo", "avatar_url"),
89+
resource.TestCheckResourceAttr("gitlab_topic.foo", "_avatar_hash", "a58bd926fd3baabd41c56e810f62ade8705d18a4e280fb35764edb4b778444db"),
90+
),
91+
},
92+
// Verify import
93+
{
94+
ResourceName: "gitlab_topic.foo",
95+
ImportState: true,
96+
ImportStateVerify: true,
97+
ImportStateVerifyIgnore: []string{
98+
"avatar", "_avatar_hash", "soft_destroy",
6099
},
61100
},
62101
// Update the topics values back to their initial state
@@ -67,6 +106,8 @@ func TestAccGitlabTopic_basic(t *testing.T) {
67106
testAccCheckGitlabTopicAttributes(&topic, &testAccGitlabTopicExpectedAttributes{
68107
Name: fmt.Sprintf("foo-req-%d", rInt),
69108
}),
109+
resource.TestCheckResourceAttr("gitlab_topic.foo", "avatar_url", ""),
110+
resource.TestCheckResourceAttr("gitlab_topic.foo", "_avatar_hash", ""),
70111
),
71112
},
72113
// Verify import
@@ -75,7 +116,7 @@ func TestAccGitlabTopic_basic(t *testing.T) {
75116
ImportState: true,
76117
ImportStateVerify: true,
77118
ImportStateVerifyIgnore: []string{
78-
"soft_destroy",
119+
"avatar", "_avatar_hash", "soft_destroy",
79120
},
80121
},
81122
// Updating the topic to have a description before it is deleted
@@ -95,7 +136,7 @@ func TestAccGitlabTopic_basic(t *testing.T) {
95136
ImportState: true,
96137
ImportStateVerify: true,
97138
ImportStateVerifyIgnore: []string{
98-
"soft_destroy",
139+
"avatar", "_avatar_hash", "soft_destroy",
99140
},
100141
},
101142
},
@@ -247,6 +288,7 @@ func testAccGitlabTopicFullConfig(rInt int) string {
247288
resource "gitlab_topic" "foo" {
248289
name = "foo-full-%d"
249290
description = "Terraform acceptance tests"
291+
avatar = "${path.module}/testdata/gitlab_topic/avatar.png"
250292
}`, rInt)
251293
}
252294

Loading
Loading

0 commit comments

Comments
 (0)