From 29e45e7aa653e8f72a3d2b123c4d2acb2676209d Mon Sep 17 00:00:00 2001 From: Patrick Rice Date: Sun, 27 Mar 2022 18:01:17 +0000 Subject: [PATCH 1/3] Add gitlab_group_project_file_template resource Add tests for gitlab_group_project_file_template Fix bad destroy test, add API workaround, update documentation Fix formatting on Upstream API documentation to pass Regex Update examples/resources/gitlab_group_project_file_template/resource.tf Co-authored-by: Timo Furrer Refactor tests to manage group and project externally Also update the method used to overwrite the omitempty and regenerate documents Removed Dead Code Fix Linting Issues Update internal/provider/resource_gitlab_group_project_file_template.go Co-authored-by: Timo Furrer Update internal/provider/resource_gitlab_group_project_file_template.go Co-authored-by: Timo Furrer Update internal/provider/resource_gitlab_group_project_file_template.go Co-authored-by: Timo Furrer Refactor test data to use helper_test --- docs/resources/group_project_file_template.md | 62 ++++++++ .../resource.tf | 18 +++ internal/provider/helper_test.go | 17 ++- ...urce_gitlab_group_project_file_template.go | 134 ++++++++++++++++++ ...gitlab_group_project_file_template_test.go | 95 +++++++++++++ .../provider/resource_gitlab_project_test.go | 9 +- 6 files changed, 326 insertions(+), 9 deletions(-) create mode 100644 docs/resources/group_project_file_template.md create mode 100644 examples/resources/gitlab_group_project_file_template/resource.tf create mode 100644 internal/provider/resource_gitlab_group_project_file_template.go create mode 100644 internal/provider/resource_gitlab_group_project_file_template_test.go diff --git a/docs/resources/group_project_file_template.md b/docs/resources/group_project_file_template.md new file mode 100644 index 000000000..e80870b4b --- /dev/null +++ b/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 + +### 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. + + diff --git a/examples/resources/gitlab_group_project_file_template/resource.tf b/examples/resources/gitlab_group_project_file_template/resource.tf new file mode 100644 index 000000000..6d8fa62ee --- /dev/null +++ b/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 +} diff --git a/internal/provider/helper_test.go b/internal/provider/helper_test.go index 040da466a..09c633f2f 100644 --- a/internal/provider/helper_test.go +++ b/internal/provider/helper_test.go @@ -118,16 +118,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) +} + +// testAccCreateProject 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) } diff --git a/internal/provider/resource_gitlab_group_project_file_template.go b/internal/provider/resource_gitlab_group_project_file_template.go new file mode 100644 index 000000000..7fb741414 --- /dev/null +++ b/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 = gitlab.Int(d.Get("file_template_project_id").(int)) + } + + _, _, 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 + }) +} diff --git a/internal/provider/resource_gitlab_group_project_file_template_test.go b/internal/provider/resource_gitlab_group_project_file_template_test.go new file mode 100644 index 000000000..48ae73753 --- /dev/null +++ b/internal/provider/resource_gitlab_group_project_file_template_test.go @@ -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) +} diff --git a/internal/provider/resource_gitlab_project_test.go b/internal/provider/resource_gitlab_project_test.go index 4b89c73f6..8e2a9347a 100644 --- a/internal/provider/resource_gitlab_project_test.go +++ b/internal/provider/resource_gitlab_project_test.go @@ -5,7 +5,6 @@ package provider import ( "errors" "fmt" - "os" "regexp" "strings" "testing" @@ -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() @@ -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() From 6d15feb05d34992a4fbfecbdab2d0785741c7fc0 Mon Sep 17 00:00:00 2001 From: Patrick Rice <50960557+PatrickRice-KSC@users.noreply.github.com> Date: Wed, 30 Mar 2022 21:52:53 -0500 Subject: [PATCH 2/3] Update internal/provider/resource_gitlab_group_project_file_template.go Co-authored-by: Timo Furrer --- .../provider/resource_gitlab_group_project_file_template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/resource_gitlab_group_project_file_template.go b/internal/provider/resource_gitlab_group_project_file_template.go index 7fb741414..621e79f96 100644 --- a/internal/provider/resource_gitlab_group_project_file_template.go +++ b/internal/provider/resource_gitlab_group_project_file_template.go @@ -85,7 +85,7 @@ func resourceGitLabGroupProjectFileTemplateCreateOrUpdate(ctx context.Context, d // 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 = gitlab.Int(d.Get("file_template_project_id").(int)) + options.FileTemplateProjectID = projectID } _, _, err := client.Groups.UpdateGroup(groupID, options) From d80bfbdc954c7d016559582714a0a9ff46a56edd Mon Sep 17 00:00:00 2001 From: Patrick Rice <50960557+PatrickRice-KSC@users.noreply.github.com> Date: Wed, 30 Mar 2022 23:21:05 -0500 Subject: [PATCH 3/3] Update internal/provider/helper_test.go Co-authored-by: Adam Snyder --- internal/provider/helper_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/helper_test.go b/internal/provider/helper_test.go index 09c633f2f..476667fa0 100644 --- a/internal/provider/helper_test.go +++ b/internal/provider/helper_test.go @@ -121,7 +121,7 @@ func testAccCreateProject(t *testing.T) *gitlab.Project { return testAccCreateProjectWithNamespace(t, 0) } -// testAccCreateProject is a test helper for creating a project. This method accepts a namespace to great a project +// 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()