Skip to content

Commit

Permalink
Merge pull request #971 from PatrickRice-KSC/add-project-file-templat…
Browse files Browse the repository at this point in the history
…e-resource

Add new resource for linking Project to Group as file_project_template
  • Loading branch information
timofurrer committed Mar 31, 2022
2 parents dfd36e5 + d80bfbd commit 9c09272
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 9 deletions.
62 changes: 62 additions & 0 deletions docs/resources/group_project_file_template.md
@@ -0,0 +1,62 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "gitlab_group_project_file_template Resource - terraform-provider-gitlab"
subcategory: ""
description: |-
The gitlab_group_project_file_template resource allows setting a project from which
custom file templates will be loaded. The project selected must be a direct child of the group identified.
For more information about which file types are available as templates, view
GitLab's documentation https://docs.gitlab.com/ee/user/admin_area/settings/instance_template_repository.html#supported-file-types-and-locations
-> This resource requires a GitLab Enterprise instance with a Premium license.
Upstream API: GitLab REST API docs https://docs.gitlab.com/ee/api/groups.html#update-group
---

# gitlab_group_project_file_template (Resource)

The `gitlab_group_project_file_template` resource allows setting a project from which
custom file templates will be loaded. The project selected must be a direct child of the group identified.
For more information about which file types are available as templates, view
[GitLab's documentation](https://docs.gitlab.com/ee/user/admin_area/settings/instance_template_repository.html#supported-file-types-and-locations)

-> This resource requires a GitLab Enterprise instance with a Premium license.

**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/groups.html#update-group)

## Example Usage

```terraform
resource "gitlab_group" "foo" {
name = "group"
path = "group"
description = "An example group"
}
resource "gitlab_project" "bar" {
name = "template project"
description = "contains file templates"
visibility_level = "public"
namespace_id = gitlab_group.foo.id
}
resource "gitlab_group_project_file_template" "template_link" {
group_id = gitlab_group.foo.id
project = gitlab_project.bar.id
}
```

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

### Required

- `file_template_project_id` (Number) The ID of the project that will be used for file templates. This project must be the direct
child of the project defined by the group_id
- `group_id` (Number) The ID of the group that will use the file template project. This group must be the direct
parent of the project defined by project_id

### Optional

- `id` (String) The ID of this resource.


18 changes: 18 additions & 0 deletions examples/resources/gitlab_group_project_file_template/resource.tf
@@ -0,0 +1,18 @@
resource "gitlab_group" "foo" {
name = "group"
path = "group"
description = "An example group"
}

resource "gitlab_project" "bar" {
name = "template project"
description = "contains file templates"
visibility_level = "public"

namespace_id = gitlab_group.foo.id
}

resource "gitlab_group_project_file_template" "template_link" {
group_id = gitlab_group.foo.id
project = gitlab_project.bar.id
}
17 changes: 15 additions & 2 deletions internal/provider/helper_test.go
Expand Up @@ -119,16 +119,29 @@ func testAccCurrentUser(t *testing.T) *gitlab.User {

// testAccCreateProject is a test helper for creating a project.
func testAccCreateProject(t *testing.T) *gitlab.Project {
return testAccCreateProjectWithNamespace(t, 0)
}

// testAccCreateProjectWithNamespace is a test helper for creating a project. This method accepts a namespace to great a project
// within a group
func testAccCreateProjectWithNamespace(t *testing.T, namespaceID int) *gitlab.Project {
t.Helper()

project, _, err := testGitlabClient.Projects.CreateProject(&gitlab.CreateProjectOptions{
options := &gitlab.CreateProjectOptions{
Name: gitlab.String(acctest.RandomWithPrefix("acctest")),
Description: gitlab.String("Terraform acceptance tests"),
// So that acceptance tests can be run in a gitlab organization with no billing.
Visibility: gitlab.Visibility(gitlab.PublicVisibility),
// So that a branch is created.
InitializeWithReadme: gitlab.Bool(true),
})
}

//Apply a namespace if one is passed in.
if namespaceID != 0 {
options.NamespaceID = gitlab.Int(namespaceID)
}

project, _, err := testGitlabClient.Projects.CreateProject(options)
if err != nil {
t.Fatalf("could not create test project: %v", err)
}
Expand Down
134 changes: 134 additions & 0 deletions internal/provider/resource_gitlab_group_project_file_template.go
@@ -0,0 +1,134 @@
package provider

import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
gitlab "github.com/xanzy/go-gitlab"
"log"
)

var _ = registerResource("gitlab_group_project_file_template", func() *schema.Resource {
return &schema.Resource{
Description: `The ` + "`gitlab_group_project_file_template`" + ` resource allows setting a project from which
custom file templates will be loaded. The project selected must be a direct child of the group identified.
For more information about which file types are available as templates, view
[GitLab's documentation](https://docs.gitlab.com/ee/user/admin_area/settings/instance_template_repository.html#supported-file-types-and-locations)
-> This resource requires a GitLab Enterprise instance with a Premium license.
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/groups.html#update-group)`,

// Since this resource updates an in-place resource, the update method is the same as the create method
CreateContext: resourceGitLabGroupProjectFileTemplateCreateOrUpdate,
UpdateContext: resourceGitLabGroupProjectFileTemplateCreateOrUpdate,
ReadContext: resourceGitLabGroupProjectFileTemplateRead,
DeleteContext: resourceGitLabGroupProjectFileTemplateDelete,
// Since this resource updates an in-place resource, importing doesn't make much sense. Simply add the resource
// to the config and terraform will overwrite what's already in place and manage it from there.
Schema: map[string]*schema.Schema{
"group_id": {
Description: `The ID of the group that will use the file template project. This group must be the direct
parent of the project defined by project_id`,
Type: schema.TypeInt,

// Even though there is no traditional resource to create, leave "ForceNew" as "true" so that if someone
// changes a configuration to a different group, the old group gets "deleted" (updated to have a value
// of 0).
ForceNew: true,
Required: true,
},
"file_template_project_id": {
Description: `The ID of the project that will be used for file templates. This project must be the direct
child of the project defined by the group_id`,
Type: schema.TypeInt,
Required: true,
},
},
}
})

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

groupID := d.Get("group_id").(int)
group, _, err := client.Groups.GetGroup(groupID, nil, gitlab.WithContext(ctx))
if err != nil {
if is404(err) {
log.Printf("[DEBUG] gitlab group %d not found, removing from state", groupID)
d.SetId("")
return nil
}
return diag.FromErr(err)
}
if group.MarkedForDeletionOn != nil {
log.Printf("[DEBUG] gitlab group %s is marked for deletion, removing from state", d.Id())
d.SetId("")
return nil
}

d.SetId(fmt.Sprintf("%d", group.ID))
d.Set("file_template_project_id", group.FileTemplateProjectID)

return nil
}

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

groupID := d.Get("group_id").(int)
projectID := gitlab.Int(d.Get("file_template_project_id").(int))

// Creating the resource means updating the existing group to link the project to the group.
options := &gitlab.UpdateGroupOptions{}
if d.HasChanges("file_template_project_id") {
options.FileTemplateProjectID = projectID
}

_, _, err := client.Groups.UpdateGroup(groupID, options)
if err != nil {
return diag.Errorf("unable to update group %d with `file_template_project_id` set to %d: %s", groupID, projectID, err)
}
return resourceGitLabGroupProjectFileTemplateRead(ctx, d, meta)
}

func resourceGitLabGroupProjectFileTemplateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)
groupID := d.Get("group_id").(int)
options := &gitlab.UpdateGroupOptions{}

_, _, err := updateGroupWithOverwrittenFileTemplateOption(client, groupID, options)
if err != nil {
return diag.Errorf("could not update group %d to remove file template ID: %s", groupID, err)
}
return resourceGitLabGroupProjectFileTemplateRead(ctx, d, meta)
}

func updateGroupWithOverwrittenFileTemplateOption(client *gitlab.Client, groupID int, options *gitlab.UpdateGroupOptions) (*gitlab.Group, *gitlab.Response, error) {
return client.Groups.UpdateGroup(groupID, options, func(request *retryablehttp.Request) error {
//Overwrite the GroupUpdateOptions struct to remove the "omitempty", which forces the client to send an empty
//string in just this request.
removeOmitEmptyOptions := struct {
FileTemplateProjectID *string `url:"file_template_project_id" json:"file_template_project_id"`
}{
FileTemplateProjectID: nil,
}

//Create the new body request with the above struct
newBody, err := json.Marshal(removeOmitEmptyOptions)
if err != nil {
return err
}

//Set the request body to have the newly updated body
err = request.SetBody(newBody)
if err != nil {
return err
}

return nil
})
}
@@ -0,0 +1,95 @@
package provider

import (
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/xanzy/go-gitlab"
"strconv"
"testing"
)

func TestAccGitlabGroupProjectFileTemplate_basic(t *testing.T) {
// Since we do some manual setup in this test, we need to handle the test skip first.
testAccCheck(t)
baseGroup := testAccCreateGroups(t, 1)[0]
firstProject := testAccCreateProjectWithNamespace(t, baseGroup.ID)
secondProject := testAccCreateProjectWithNamespace(t, baseGroup.ID)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckProjectFileTemplateDestroy,
Steps: []resource.TestStep{
{
SkipFunc: isRunningInCE,
Config: testAccGroupProjectFileTemplateConfig(baseGroup.ID, firstProject.ID),
Check: resource.ComposeTestCheckFunc(
// Note - we can't use the testAccCheckGitlabGroupAttributes, because that checks the TF
// state attributes, and file project template explicitly doesn't exist there.
testAccCheckGitlabGroupFileTemplateValue(baseGroup, firstProject),
resource.TestCheckResourceAttr("gitlab_group_project_file_template.linking_template", "group_id", strconv.Itoa(baseGroup.ID)),
resource.TestCheckResourceAttr("gitlab_group_project_file_template.linking_template", "file_template_project_id", strconv.Itoa(firstProject.ID)),
),
},
{
//Test that when we update the project name, it re-links the group to the new project
SkipFunc: isRunningInCE,
Config: testAccGroupProjectFileTemplateConfig(baseGroup.ID, secondProject.ID),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabGroupFileTemplateValue(baseGroup, secondProject),
resource.TestCheckResourceAttr("gitlab_group_project_file_template.linking_template", "group_id", strconv.Itoa(baseGroup.ID)),
resource.TestCheckResourceAttr("gitlab_group_project_file_template.linking_template", "file_template_project_id", strconv.Itoa(secondProject.ID)),
),
},
},
},
)
}

func testAccCheckGitlabGroupFileTemplateValue(g *gitlab.Group, p *gitlab.Project) resource.TestCheckFunc {
return func(s *terraform.State) error {
//Re-retrieve the group to ensure we have the most up-to-date group info
g, _, err := testGitlabClient.Groups.GetGroup(g.ID, &gitlab.GetGroupOptions{})
if is404(err) {
return fmt.Errorf("Group no longer exists, expected group to exist with a file_template_project_id")
}

if g.FileTemplateProjectID == p.ID {
return nil
}
return fmt.Errorf("Group file_template_project_id doesn't match. Wanted %d, received %d", p.ID, g.FileTemplateProjectID)
}
}

func testAccCheckProjectFileTemplateDestroy(state *terraform.State) error {
for _, rs := range state.RootModule().Resources {
if rs.Type != "gitlab_group_project_file_template" {
continue
}

// To test if the resource was destroyed, we need to retrieve the group.
gid := rs.Primary.ID
group, _, err := testGitlabClient.Groups.GetGroup(gid, nil)
if err != nil {
return err
}

// the test should succeed if the group is still present and has a 0 file_template_project_id value
if group != nil && group.FileTemplateProjectID != 0 {
return fmt.Errorf("Group still has a template project attached")
}
return nil
}
return nil
}

func testAccGroupProjectFileTemplateConfig(groupID int, projectID int) string {
return fmt.Sprintf(
`
resource "gitlab_group_project_file_template" "linking_template" {
group_id = %d
file_template_project_id = %d
}
`, groupID, projectID)
}
9 changes: 2 additions & 7 deletions internal/provider/resource_gitlab_project_test.go
Expand Up @@ -5,7 +5,6 @@ package provider
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -773,9 +772,7 @@ func TestAccGitlabProject_transfer(t *testing.T) {
// lintignore: AT002 // not a Terraform import test
func TestAccGitlabProject_importURL(t *testing.T) {
// Since we do some manual setup in this test, we need to handle the test skip first.
if os.Getenv(resource.EnvTfAcc) == "" {
t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", resource.EnvTfAcc))
}
testAccCheck(t)

rInt := acctest.RandInt()

Expand Down Expand Up @@ -901,9 +898,7 @@ func testAccCheckGitlabProjectMirroredAttributes(project *gitlab.Project, want *
// lintignore: AT002 // not a Terraform import test
func TestAccGitlabProject_importURLMirrored(t *testing.T) {
// Since we do some manual setup in this test, we need to handle the test skip first.
if os.Getenv(resource.EnvTfAcc) == "" {
t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", resource.EnvTfAcc))
}
testAccCheck(t)

var mirror gitlab.Project
rInt := acctest.RandInt()
Expand Down

0 comments on commit 9c09272

Please sign in to comment.