From 2c60d93290bbf9e2498b91b1a7dbb6c81cf247f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Funt=C3=B2?= Date: Thu, 6 Jun 2019 14:12:42 +0200 Subject: [PATCH 1/4] Implement project's archived attribute --- gitlab/resource_gitlab_project.go | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/gitlab/resource_gitlab_project.go b/gitlab/resource_gitlab_project.go index c7eb1d069..088c09880 100644 --- a/gitlab/resource_gitlab_project.go +++ b/gitlab/resource_gitlab_project.go @@ -142,6 +142,12 @@ var resourceGitLabProjectSchema = map[string]*schema.Schema{ }, }, }, + "archived": { + Type: schema.TypeBool, + Description: "Whether the project is archived.", + Optional: true, + Default: false, + }, } func resourceGitlabProject() *schema.Resource { @@ -181,6 +187,7 @@ func resourceGitlabProjectSetToState(d *schema.ResourceData, project *gitlab.Pro d.Set("shared_runners_enabled", project.SharedRunnersEnabled) d.Set("shared_with_groups", flattenSharedWithGroupsOptions(project)) d.Set("tags", project.TagList) + d.Set("archived", project.Archived) } func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error { @@ -233,6 +240,16 @@ func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error d.SetId(fmt.Sprintf("%d", project.ID)) + v := d.Get("archived") + if v.(bool) { + // strange as it may seem, this project is created in archived state... + err := archiveProject(d, meta) + if err != nil { + log.Printf("[WARN] New project (%s) could not be created in archived state: error %#v", d.Id(), err) + // TODO: handle error instead of swallowing it? + } + } + return resourceGitlabProjectRead(d, meta) } @@ -330,6 +347,23 @@ func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error updateSharedWithGroups(d, meta) } + if d.HasChange("archived") { + v := d.Get("archived") + if v.(bool) { + err := archiveProject(d, meta) + if err != nil { + log.Printf("[WARN] Project (%s) could not be archived: error %#v", d.Id(), err) + return err + } + } else { + err := unarchiveProject(d, meta) + if err != nil { + log.Printf("[WARN] Project (%s) could not be unarchived: error %#v", d.Id(), err) + return err + } + } + } + return resourceGitlabProjectRead(d, meta) } @@ -488,3 +522,41 @@ func updateSharedWithGroups(d *schema.ResourceData, meta interface{}) error { return nil } + +// archiveProject calls the Gitlab server to archive a project; if the +// project is already archived, the call will do nothing (the API is +// idempotent). +func archiveProject(d *schema.ResourceData, meta interface{}) error { + log.Printf("[TRACE] Project (%s) will be archived", d.Id()) + client := meta.(*gitlab.Client) + out, _, err := client.Projects.ArchiveProject(d.Id()) + if err != nil { + log.Printf("[ERROR] Error archiving project (%s), received %#v", d.Id(), err) + return err + } + if !out.Archived { + log.Printf("[ERROR] Project (%s) is still not archived", d.Id()) + return fmt.Errorf("error archiving project (%s): its status on the server is still unarchived", d.Id()) + } + log.Printf("[TRACE] Project (%s) archived", d.Id()) + return nil +} + +// unarchiveProject calls the Gitlab server to unarchive a project; if the +// project is already not archived, the call will do nothing (the API is +// idempotent). +func unarchiveProject(d *schema.ResourceData, meta interface{}) error { + log.Printf("[INFO] Project (%s) will be unarchived", d.Id()) + client := meta.(*gitlab.Client) + out, _, err := client.Projects.UnarchiveProject(d.Id()) + if err != nil { + log.Printf("[ERROR] Error unarchiving project (%s), received %#v", d.Id(), err) + return err + } + if out.Archived { + log.Printf("[ERROR] Project (%s) is still archived", d.Id()) + return fmt.Errorf("error unarchiving project (%s): its status on the server is still archived", d.Id()) + } + log.Printf("[TRACE] Project (%s) unarchived", d.Id()) + return nil +} From 24a689fb195f98cd79b0b32f16068b48a1dcfa63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Funt=C3=B2?= Date: Thu, 6 Jun 2019 16:38:00 +0200 Subject: [PATCH 2/4] Added management of partial state update to Project resource --- gitlab/data_source_gitlab_project.go | 6 ++++++ gitlab/resource_gitlab_project.go | 28 ++++++++++++++++++++++++++ gitlab/resource_gitlab_project_test.go | 11 ++++++---- website/docs/d/project.html.markdown | 2 ++ website/docs/r/project.html.markdown | 2 ++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/gitlab/data_source_gitlab_project.go b/gitlab/data_source_gitlab_project.go index 9434f4188..f46d8d8a5 100644 --- a/gitlab/data_source_gitlab_project.go +++ b/gitlab/data_source_gitlab_project.go @@ -87,6 +87,11 @@ func dataSourceGitlabProject() *schema.Resource { Optional: true, Computed: true, }, + "archived": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, }, } } @@ -118,5 +123,6 @@ func dataSourceGitlabProjectRead(d *schema.ResourceData, meta interface{}) error d.Set("http_url_to_repo", found.HTTPURLToRepo) d.Set("web_url", found.WebURL) d.Set("runners_token", found.RunnersToken) + d.Set("archived", found.Archived) return nil } diff --git a/gitlab/resource_gitlab_project.go b/gitlab/resource_gitlab_project.go index 088c09880..eadc812a2 100644 --- a/gitlab/resource_gitlab_project.go +++ b/gitlab/resource_gitlab_project.go @@ -271,68 +271,89 @@ func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error options := &gitlab.EditProjectOptions{} + // need to manage partial state since project archiving requires + // a separate API call which could fail + d.Partial(true) + updatedProperties := []string{} + if d.HasChange("name") { options.Name = gitlab.String(d.Get("name").(string)) + updatedProperties = append(updatedProperties, "name") } if d.HasChange("path") && (d.Get("path").(string) != "") { options.Path = gitlab.String(d.Get("path").(string)) + updatedProperties = append(updatedProperties, "path") } if d.HasChange("description") { options.Description = gitlab.String(d.Get("description").(string)) + updatedProperties = append(updatedProperties, "description") } if d.HasChange("default_branch") { options.DefaultBranch = gitlab.String(d.Get("default_branch").(string)) + updatedProperties = append(updatedProperties, "default_branch") } if d.HasChange("visibility_level") { options.Visibility = stringToVisibilityLevel(d.Get("visibility_level").(string)) + updatedProperties = append(updatedProperties, "visibility_level") } if d.HasChange("merge_method") { options.MergeMethod = stringToMergeMethod(d.Get("merge_method").(string)) + updatedProperties = append(updatedProperties, "merge_method") } if d.HasChange("only_allow_merge_if_pipeline_succeeds") { options.OnlyAllowMergeIfPipelineSucceeds = gitlab.Bool(d.Get("only_allow_merge_if_pipeline_succeeds").(bool)) + updatedProperties = append(updatedProperties, "only_allow_merge_if_pipeline_succeeds") } if d.HasChange("only_allow_merge_if_all_discussions_are_resolved") { options.OnlyAllowMergeIfAllDiscussionsAreResolved = gitlab.Bool(d.Get("only_allow_merge_if_all_discussions_are_resolved").(bool)) + updatedProperties = append(updatedProperties, "only_allow_merge_if_all_discussions_are_resolved") } if d.HasChange("issues_enabled") { options.IssuesEnabled = gitlab.Bool(d.Get("issues_enabled").(bool)) + updatedProperties = append(updatedProperties, "issues_enabled") } if d.HasChange("merge_requests_enabled") { options.MergeRequestsEnabled = gitlab.Bool(d.Get("merge_requests_enabled").(bool)) + updatedProperties = append(updatedProperties, "merge_requests_enabled") } if d.HasChange("approvals_before_merge") { options.ApprovalsBeforeMerge = gitlab.Int(d.Get("approvals_before_merge").(int)) + updatedProperties = append(updatedProperties, "approvals_before_merge") } if d.HasChange("wiki_enabled") { options.WikiEnabled = gitlab.Bool(d.Get("wiki_enabled").(bool)) + updatedProperties = append(updatedProperties, "wiki_enabled") } if d.HasChange("snippets_enabled") { options.SnippetsEnabled = gitlab.Bool(d.Get("snippets_enabled").(bool)) + updatedProperties = append(updatedProperties, "snippets_enabled") } if d.HasChange("shared_runners_enabled") { options.SharedRunnersEnabled = gitlab.Bool(d.Get("shared_runners_enabled").(bool)) + updatedProperties = append(updatedProperties, "shared_runners_enabled") } if d.HasChange("tags") { options.TagList = stringSetToStringSlice(d.Get("tags").(*schema.Set)) + updatedProperties = append(updatedProperties, "tags") } if d.HasChange("container_registry_enabled") { options.ContainerRegistryEnabled = gitlab.Bool(d.Get("container_registry_enabled").(bool)) + updatedProperties = append(updatedProperties, "container_registry_enabled") } if *options != (gitlab.EditProjectOptions{}) { @@ -341,8 +362,13 @@ func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error if err != nil { return err } + for _, updatedProperty := range updatedProperties { + log.Printf("[DEBUG] partial gitlab project %s update of property %q", d.Id(), updatedProperty) + d.SetPartial(updatedProperty) + } } + // TODO: handle partial state update if d.HasChange("shared_with_groups") { updateSharedWithGroups(d, meta) } @@ -362,8 +388,10 @@ func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error return err } } + d.SetPartial("archived") } + d.Partial(false) return resourceGitlabProjectRead(d, meta) } diff --git a/gitlab/resource_gitlab_project_test.go b/gitlab/resource_gitlab_project_test.go index 3312b16d0..9f7bfd30f 100644 --- a/gitlab/resource_gitlab_project_test.go +++ b/gitlab/resource_gitlab_project_test.go @@ -32,6 +32,7 @@ func TestAccGitlabProject_basic(t *testing.T) { MergeMethod: gitlab.FastForwardMerge, OnlyAllowMergeIfPipelineSucceeds: true, OnlyAllowMergeIfAllDiscussionsAreResolved: true, + Archived: false, // needless, but let's make this explicit } resource.Test(t, resource.TestCase{ @@ -39,7 +40,7 @@ func TestAccGitlabProject_basic(t *testing.T) { Providers: testAccProviders, CheckDestroy: testAccCheckGitlabProjectDestroy, Steps: []resource.TestStep{ - // Step0 Create a project with all the features on + // Step0 Create a project with all the features on (note: "archived" is "false") { Config: testAccGitlabProjectConfig(rInt), Check: resource.ComposeTestCheckFunc( @@ -47,7 +48,7 @@ func TestAccGitlabProject_basic(t *testing.T) { testAccCheckAggregateGitlabProject(&defaults, &received), ), }, - // Step1 Update the project to turn the features off + // Step1 Update the project to turn the features off (note: "archived" is "true") { Config: testAccGitlabProjectUpdateConfig(rInt), Check: resource.ComposeTestCheckFunc( @@ -65,10 +66,11 @@ func TestAccGitlabProject_basic(t *testing.T) { MergeMethod: gitlab.FastForwardMerge, OnlyAllowMergeIfPipelineSucceeds: true, OnlyAllowMergeIfAllDiscussionsAreResolved: true, + Archived: true, }, &received), ), }, - // Step2 Update the project to turn the features on again + // Step2 Update the project to turn the features on again (note: "archived" is "false") { Config: testAccGitlabProjectConfig(rInt), Check: resource.ComposeTestCheckFunc( @@ -411,7 +413,8 @@ resource "gitlab_project" "foo" { wiki_enabled = false snippets_enabled = false container_registry_enabled = false - shared_runners_enabled = false + shared_runners_enabled = false + archived = true } `, rInt, rInt) } diff --git a/website/docs/d/project.html.markdown b/website/docs/d/project.html.markdown index 9ccba3ca4..5aa4fb01a 100644 --- a/website/docs/d/project.html.markdown +++ b/website/docs/d/project.html.markdown @@ -58,3 +58,5 @@ The following attributes are exported: * `web_url` - URL that can be used to find the project in a browser. * `runners_token` - Registration token to use during runner setup. + +* `archived` - Whether the project is in read-only mode (archived). diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 422732797..3f0bc04cc 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -71,6 +71,8 @@ The following arguments are supported: * `group_access_level` - (Optional) Group's sharing permissions. See [group members permission][group_members_permissions] for more info. Valid values are `guest`, `reporter`, `developer`, `master`. +* `archived` - (Optional) Whether the project is in read-only mode (archived). Repositories can be archived/unarchived by toggling this parameter. + ## Attributes Reference The following additional attributes are exported: From fa60e8a1a530236d109143efb0afe44535f8409a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Funt=C3=B2?= Date: Thu, 6 Jun 2019 16:45:16 +0200 Subject: [PATCH 3/4] Implement error checking on Project's shared_groups update management --- gitlab/resource_gitlab_project.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gitlab/resource_gitlab_project.go b/gitlab/resource_gitlab_project.go index eadc812a2..cdbca4d1a 100644 --- a/gitlab/resource_gitlab_project.go +++ b/gitlab/resource_gitlab_project.go @@ -368,9 +368,13 @@ func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error } } - // TODO: handle partial state update if d.HasChange("shared_with_groups") { - updateSharedWithGroups(d, meta) + err := updateSharedWithGroups(d, meta) + // TODO: check if handling partial state update in this simplistic + // way is ok when an error in the "shared groups" API calls occurs + if err != nil { + d.SetPartial("shared_with_groups") + } } if d.HasChange("archived") { From a56428050dcdf6bdb2f19bba07915ee355a6e941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Funt=C3=B2?= Date: Tue, 11 Jun 2019 08:37:28 +0200 Subject: [PATCH 4/4] Implement partial state management in resourceGitlabProjectCreate; fix documentation error --- gitlab/resource_gitlab_project.go | 42 ++++++++++++++++++++++++++-- website/docs/r/project.html.markdown | 2 +- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/gitlab/resource_gitlab_project.go b/gitlab/resource_gitlab_project.go index cdbca4d1a..e7418864b 100644 --- a/gitlab/resource_gitlab_project.go +++ b/gitlab/resource_gitlab_project.go @@ -192,6 +192,7 @@ func resourceGitlabProjectSetToState(d *schema.ResourceData, project *gitlab.Pro func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*gitlab.Client) + options := &gitlab.CreateProjectOptions{ Name: gitlab.String(d.Get("name").(string)), IssuesEnabled: gitlab.Bool(d.Get("issues_enabled").(bool)), @@ -207,20 +208,43 @@ func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error SharedRunnersEnabled: gitlab.Bool(d.Get("shared_runners_enabled").(bool)), } + // need to manage partial state since project creation may require + // more than a single API call, and they may all fail independently; + // the default set of attributes is prepopulated with those used above + d.Partial(true) + setProperties := []string{ + "name", + "issues_enabled", + "merge_requests_enabled", + "approvals_before_merge", + "wiki_enabled", + "snippets_enabled", + "container_registry_enabled", + "visibility_level", + "merge_method", + "only_allow_merge_if_pipeline_succeeds", + "only_allow_merge_if_all_discussions_are_resolved", + "shared_runners_enabled", + } + if v, ok := d.GetOk("path"); ok { options.Path = gitlab.String(v.(string)) + setProperties = append(setProperties, "path") } if v, ok := d.GetOk("namespace_id"); ok { options.NamespaceID = gitlab.Int(v.(int)) + setProperties = append(setProperties, "namespace_id") } if v, ok := d.GetOk("description"); ok { options.Description = gitlab.String(v.(string)) + setProperties = append(setProperties, "description") } if v, ok := d.GetOk("tags"); ok { options.TagList = stringSetToStringSlice(v.(*schema.Set)) + setProperties = append(setProperties, "tags") } log.Printf("[DEBUG] create gitlab project %q", *options.Name) @@ -230,26 +254,38 @@ func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error return err } + for _, setProperty := range setProperties { + log.Printf("[DEBUG] partial gitlab project %s creation of property %q", d.Id(), setProperty) + d.SetPartial(setProperty) + } + + // from this point onwards no matter how we return, resource creation + // is committed to state since we set its ID + d.SetId(fmt.Sprintf("%d", project.ID)) + if v, ok := d.GetOk("shared_with_groups"); ok { for _, option := range expandSharedWithGroupsOptions(v) { if _, err := client.Projects.ShareProjectWithGroup(project.ID, option); err != nil { return err } } + d.SetPartial("shared_with_groups") } - d.SetId(fmt.Sprintf("%d", project.ID)) - v := d.Get("archived") if v.(bool) { // strange as it may seem, this project is created in archived state... err := archiveProject(d, meta) if err != nil { log.Printf("[WARN] New project (%s) could not be created in archived state: error %#v", d.Id(), err) - // TODO: handle error instead of swallowing it? + return err } + d.SetPartial(("archived")) } + // everything went OK, we can revert to ordinary state management + // and let the Gitlab server fill in the resource state via a read + d.Partial(false) return resourceGitlabProjectRead(d, meta) } diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 3f0bc04cc..bec9e9229 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -68,7 +68,7 @@ The following arguments are supported: * `shared_with_groups` - (Optional) Enable sharing the project with a list of groups (maps). * `group_id` - (Required) Group id of the group you want to share the project with. - * `group_access_level` - (Optional) Group's sharing permissions. See [group members permission][group_members_permissions] for more info. + * `group_access_level` - (Required) Group's sharing permissions. See [group members permission][group_members_permissions] for more info. Valid values are `guest`, `reporter`, `developer`, `master`. * `archived` - (Optional) Whether the project is in read-only mode (archived). Repositories can be archived/unarchived by toggling this parameter.