Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/resources/topic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "gitlab_topic Resource - terraform-provider-gitlab"
subcategory: ""
description: |-
This resource allows you to create and manage topics that are then assignable to projects. Topics are the successors for project tags. Aside from avoiding terminology collisions with Git tags, they are more descriptive and better searchable.
For assigning topics, use the project ./project.md resource.
~> Deleting a resource doesn't delete the corresponding topic as the GitLab API doesn't support deleting topics yet. You can set soft_destroy to true if you want the topics description to be emptied instead.
---

# gitlab_topic (Resource)

This resource allows you to create and manage topics that are then assignable to projects. Topics are the successors for project tags. Aside from avoiding terminology collisions with Git tags, they are more descriptive and better searchable.

For assigning topics, use the [project](./project.md) resource.

~> Deleting a resource doesn't delete the corresponding topic as the GitLab API doesn't support deleting topics yet. You can set soft_destroy to true if you want the topics description to be emptied instead.

## Example Usage

```terraform
resource "gitlab_topic" "functional_programming" {
name = "Functional Programming"
description = "In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions."
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- **name** (String) The topic's name
- **soft_destroy** (Boolean) Empty the topics fields instead of deleting it

### Optional

- **description** (String) A text describing the topic
- **id** (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
# You can import a topic to terraform state using `terraform import <resource> <id>`.
# The `id` must be an integer for the id of the topic you want to import,
# for example:
terraform import gitlab_topic.functional_programming 1
```
4 changes: 4 additions & 0 deletions examples/resources/gitlab_topic/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# You can import a topic to terraform state using `terraform import <resource> <id>`.
# The `id` must be an integer for the id of the topic you want to import,
# for example:
terraform import gitlab_topic.functional_programming 1
4 changes: 4 additions & 0 deletions examples/resources/gitlab_topic/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "gitlab_topic" "functional_programming" {
name = "Functional Programming"
description = "In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions."
}
148 changes: 148 additions & 0 deletions internal/provider/resource_gitlab_topic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package provider

import (
"context"
"fmt"
"log"
"strconv"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/xanzy/go-gitlab"
)

var _ = registerResource("gitlab_topic", func() *schema.Resource {
return &schema.Resource{
Description: `This resource allows you to create and manage topics that are then assignable to projects. Topics are the successors for project tags. Aside from avoiding terminology collisions with Git tags, they are more descriptive and better searchable.

For assigning topics, use the [project](./project.md) resource.

~> Deleting a resource doesn't delete the corresponding topic as the GitLab API doesn't support deleting topics yet. You can set soft_destroy to true if you want the topics description to be emptied instead.`,

CreateContext: resourceGitlabTopicCreate,
ReadContext: resourceGitlabTopicRead,
UpdateContext: resourceGitlabTopicUpdate,
DeleteContext: resourceGitlabTopicDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"name": {
Description: "The topic's name",
Type: schema.TypeString,
Required: true,
},
"soft_destroy": {
Description: "Empty the topics fields instead of deleting it",
Type: schema.TypeBool,
Required: true,
},
"description": {
Description: "A text describing the topic",
Type: schema.TypeString,
Optional: true,
},
},
}
})

func resourceGitlabTopicCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)
options := &gitlab.CreateTopicOptions{
Name: gitlab.String(d.Get("name").(string)),
}

if v, ok := d.GetOk("description"); ok {
options.Description = gitlab.String(v.(string))
}

log.Printf("[DEBUG] create gitlab topic %s", *options.Name)

topic, _, err := client.Topics.CreateTopic(options, gitlab.WithContext(ctx))
if err != nil {
return diag.Errorf("Failed to create topic %q: %s", *options.Name, err)
}

d.SetId(fmt.Sprintf("%d", topic.ID))

return resourceGitlabTopicRead(ctx, d, meta)
}

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

topicID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.Errorf("Failed to convert topic id %s to int: %s", d.Id(), err)
}
log.Printf("[DEBUG] read gitlab topic %d", topicID)

topic, _, err := client.Topics.GetTopic(topicID, gitlab.WithContext(ctx))
if err != nil {
if is404(err) {
log.Printf("[DEBUG] gitlab group %s not found so removing from state", d.Id())
d.SetId("")
return nil
}
return diag.Errorf("Failed to read topic %d: %s", topicID, err)
}

d.SetId(fmt.Sprintf("%d", topic.ID))
d.Set("name", topic.Name)
d.Set("description", topic.Description)

return nil
}

func resourceGitlabTopicUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)
options := &gitlab.UpdateTopicOptions{}

if d.HasChange("name") {
options.Name = gitlab.String(d.Get("name").(string))
}

if d.HasChange("description") {
options.Description = gitlab.String(d.Get("description").(string))
}

log.Printf("[DEBUG] update gitlab topic %s", d.Id())

topicID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.Errorf("Failed to convert topic id %s to int: %s", d.Id(), err)
}
_, _, err = client.Topics.UpdateTopic(topicID, options, gitlab.WithContext(ctx))
if err != nil {
return diag.Errorf("Failed to update topic %d: %s", topicID, err)
}

return resourceGitlabTopicRead(ctx, d, meta)
}

func resourceGitlabTopicDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {

softDestroy := d.Get("soft_destroy").(bool)

if !softDestroy {
return diag.Errorf("Destroying a topic is not yet supported. You can set soft_destroy=true to suppress this error")
}

log.Printf("[WARN] Not deleting gitlab topic %s. Instead emptying its description", d.Id())

client := meta.(*gitlab.Client)
options := &gitlab.UpdateTopicOptions{
Description: gitlab.String(""),
}

topicID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.Errorf("Failed to convert topic id %s to int: %s", d.Id(), err)
}
_, _, err = client.Topics.UpdateTopic(topicID, options, gitlab.WithContext(ctx))
if err != nil {
return diag.Errorf("Failed to update topic %d: %s", topicID, err)
}
return nil
}
177 changes: 177 additions & 0 deletions internal/provider/resource_gitlab_topic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package provider

import (
"fmt"
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/xanzy/go-gitlab"
)

func TestAccGitlabTopic_basic(t *testing.T) {
var topic gitlab.Topic
rInt := acctest.RandInt()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckGitlabTopicDestroy,
Steps: []resource.TestStep{
// Create a topic with default options
{
Config: testAccGitlabTopicRequiredConfig(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabTopicExists("gitlab_topic.foo", &topic),
testAccCheckGitlabTopicAttributes(&topic, &testAccGitlabTopicExpectedAttributes{
Name: fmt.Sprintf("foo-req-%d", rInt),
}),
),
},
// Update the topics values
{
Config: testAccGitlabTopicFullConfig(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabTopicExists("gitlab_topic.foo", &topic),
testAccCheckGitlabTopicAttributes(&topic, &testAccGitlabTopicExpectedAttributes{
Name: fmt.Sprintf("foo-full-%d", rInt),
Description: "Terraform acceptance tests",
}),
),
},
// Update the topics values back to their initial state
{
Config: testAccGitlabTopicRequiredConfig(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabTopicExists("gitlab_topic.foo", &topic),
testAccCheckGitlabTopicAttributes(&topic, &testAccGitlabTopicExpectedAttributes{
Name: fmt.Sprintf("foo-req-%d", rInt),
}),
),
},
// Updating the topic to have a description before it is deleted
{
Config: testAccGitlabTopicFullConfig(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabTopicExists("gitlab_topic.foo", &topic),
testAccCheckGitlabTopicAttributes(&topic, &testAccGitlabTopicExpectedAttributes{
Name: fmt.Sprintf("foo-full-%d", rInt),
Description: "Terraform acceptance tests",
}),
),
},
// Verify import
{
ResourceName: "gitlab_topic.foo",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"soft_destroy",
},
},
},
})
}

func testAccCheckGitlabTopicExists(n string, assign *gitlab.Topic) resource.TestCheckFunc {
return func(s *terraform.State) (err error) {

defer func() {
if err != nil {
err = fmt.Errorf("checking for gitlab topic existence failed: %w", err)
}
}()

rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("not Found: %s", n)
}

id, err := strconv.Atoi(rs.Primary.ID)
if err != nil {
return err
}

topic, _, err := testGitlabClient.Topics.GetTopic(id)
*assign = *topic

return err
}
}

type testAccGitlabTopicExpectedAttributes struct {
Name string
Description string
SoftDestroy bool
}

func testAccCheckGitlabTopicAttributes(topic *gitlab.Topic, want *testAccGitlabTopicExpectedAttributes) resource.TestCheckFunc {
return func(s *terraform.State) error {
if topic.Name != want.Name {
return fmt.Errorf("got name %q; want %q", topic.Name, want.Name)
}

if topic.Description != want.Description {
return fmt.Errorf("got description %q; want %q", topic.Description, want.Description)
}

return nil
}
}

func testAccCheckGitlabTopicDestroy(s *terraform.State) (err error) {

defer func() {
if err != nil {
err = fmt.Errorf("destroying gitlab topic failed: %w", err)
}
}()

for _, rs := range s.RootModule().Resources {
if rs.Type != "gitlab_topic" {
continue
}

id, err := strconv.Atoi(rs.Primary.ID)
if err != nil {
return err
}

topic, resp, err := testGitlabClient.Topics.GetTopic(id)
if err == nil {
if topic != nil && fmt.Sprintf("%d", topic.ID) == rs.Primary.ID {

if topic.Description != "" {
return fmt.Errorf("topic still has a description")
}

// TODO: Return error as soon as deleting a topic is supported
return nil
}
}
if resp.StatusCode != 404 {
return err
}
return nil
}
return nil
}

func testAccGitlabTopicRequiredConfig(rInt int) string {
return fmt.Sprintf(`
resource "gitlab_topic" "foo" {
name = "foo-req-%d"
soft_destroy = true
}`, rInt)
}

func testAccGitlabTopicFullConfig(rInt int) string {
return fmt.Sprintf(`
resource "gitlab_topic" "foo" {
name = "foo-full-%d"
description = "Terraform acceptance tests"
soft_destroy = true
}`, rInt)
}