Skip to content

Commit 4ba0418

Browse files
committed
Add resource milestone
1 parent 0ec0886 commit 4ba0418

File tree

3 files changed

+488
-0
lines changed

3 files changed

+488
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package provider
2+
3+
import (
4+
"fmt"
5+
"context"
6+
"log"
7+
"strconv"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
gitlab "github.com/xanzy/go-gitlab"
12+
)
13+
14+
var milestoneStateToStateEvent = map[string]string{
15+
"active": "activate",
16+
"closed": "close",
17+
}
18+
19+
var _ = registerResource("gitlab_project_milestone", func() *schema.Resource {
20+
return &schema.Resource{
21+
Description: `The ` + "`gitlab_project_milestome`" + ` resource allows to manage the lifecycle of a milestone (project).
22+
23+
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/milestones.html)`,
24+
25+
CreateContext: resourceGitlabProjectMilestoneCreate,
26+
ReadContext: resourceGitlabProjectMilestoneRead,
27+
UpdateContext: resourceGitlabProjectMilestoneUpdate,
28+
DeleteContext: resourceGitlabProjectMilestoneDelete,
29+
Importer: &schema.ResourceImporter{
30+
StateContext: schema.ImportStatePassthroughContext,
31+
},
32+
Schema: constructSchema(
33+
gitlabProjectMilestoneGetSchema(),
34+
),
35+
}
36+
})
37+
38+
func resourceGitlabProjectMilestoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
39+
client := meta.(*gitlab.Client)
40+
project := d.Get("project_id").(string)
41+
title := d.Get("title").(string)
42+
43+
options := &gitlab.CreateMilestoneOptions{
44+
Title: &title,
45+
}
46+
if description, ok := d.GetOk("description"); ok {
47+
options.Description = gitlab.String(description.(string))
48+
}
49+
if startDate, ok := d.GetOk("start_date"); ok {
50+
parsedStartDate, err := parseISO8601Date(startDate.(string))
51+
if err != nil {
52+
return diag.Errorf("Failed to parse start_date: %s. %v", startDate.(string), err)
53+
}
54+
options.StartDate = parsedStartDate
55+
}
56+
if dueDate, ok := d.GetOk("due_date"); ok {
57+
parsedDueDate, err := parseISO8601Date(dueDate.(string))
58+
if err != nil {
59+
return diag.Errorf("Failed to parse due_date: %s. %v", dueDate.(string), err)
60+
}
61+
options.DueDate = parsedDueDate
62+
}
63+
64+
log.Printf("[DEBUG] create gitlab milestone in project %s with title %s", project, title)
65+
milestone, resp, err := client.Milestones.CreateMilestone(project, options, gitlab.WithContext(ctx))
66+
if err != nil {
67+
log.Printf("[WARN] failed to create gitlab milestone in project %s with title %s (response %v)", project, title, resp)
68+
return diag.FromErr(err)
69+
}
70+
d.SetId(resourceGitLabProjectMilestoneBuildId(project, milestone.ID))
71+
72+
updateOptions := gitlab.UpdateMilestoneOptions{}
73+
if stateEvent, ok := d.GetOk("state"); ok {
74+
updateOptions.StateEvent = gitlab.String(milestoneStateToStateEvent[stateEvent.(string)])
75+
}
76+
if updateOptions != (gitlab.UpdateMilestoneOptions{}) {
77+
_, _, err := client.Milestones.UpdateMilestone(project, milestone.ID, &updateOptions, gitlab.WithContext(ctx))
78+
if err != nil {
79+
return diag.Errorf("Failed to update milestone ID %d in project %s right after creation: %v", milestone.ID, project, err)
80+
}
81+
}
82+
83+
return resourceGitlabProjectMilestoneRead(ctx, d, meta)
84+
}
85+
86+
func resourceGitlabProjectMilestoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
87+
client := meta.(*gitlab.Client)
88+
project, milestoneID, err := resourceGitLabProjectIssueParseId(d.Id())
89+
if err != nil {
90+
return diag.FromErr(err)
91+
}
92+
93+
log.Printf("[DEBUG] read gitlab milestone in project %s with ID %d", project, milestoneID)
94+
milestone, resp, err := client.Milestones.GetMilestone(project, milestoneID, gitlab.WithContext(ctx))
95+
if err != nil {
96+
if is404(err) {
97+
log.Printf("[WARN] recieved 404 for gitlab milestone ID %d in project %s, removing from state", milestoneID, project)
98+
d.SetId("")
99+
return diag.FromErr(err)
100+
}
101+
log.Printf("[WARN] failed to read gitlab milestone ID %d in project %s. Response %v", milestoneID, project, resp)
102+
return diag.FromErr(err)
103+
}
104+
105+
stateMap := gitlabProjectMilestoneToStateMap(milestone)
106+
if err = setStateMapInResourceData(stateMap, d); err != nil {
107+
return diag.FromErr(err)
108+
}
109+
return nil
110+
}
111+
112+
func resourceGitlabProjectMilestoneUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
113+
client := meta.(*gitlab.Client)
114+
project, milestoneID, err := resourceGitLabProjectIssueParseId(d.Id())
115+
if err != nil {
116+
return diag.FromErr(err)
117+
}
118+
119+
options := &gitlab.UpdateMilestoneOptions{}
120+
if d.HasChange("title") {
121+
options.Title = gitlab.String(d.Get("title").(string))
122+
}
123+
if d.HasChange("description") {
124+
options.Description = gitlab.String(d.Get("description").(string))
125+
}
126+
if d.HasChange("start_date") {
127+
startDate := d.Get("start_date").(string)
128+
parsedStartDate, err := parseISO8601Date(startDate)
129+
if err != nil {
130+
return diag.Errorf("Failed to parse due_date: %s. %v", startDate, err)
131+
}
132+
options.StartDate = parsedStartDate
133+
}
134+
if d.HasChange("due_date") {
135+
dueDate := d.Get("due_date").(string)
136+
parsedDueDate, err := parseISO8601Date(dueDate)
137+
if err != nil {
138+
return diag.Errorf("Failed to parse due_date: %s. %v", dueDate, err)
139+
}
140+
options.DueDate = parsedDueDate
141+
}
142+
if d.HasChange("state") {
143+
options.StateEvent = gitlab.String(milestoneStateToStateEvent[d.Get("state").(string)])
144+
}
145+
146+
log.Printf("[DEBUG] update gitlab milestone in project %s with ID %d", project, milestoneID)
147+
_, _, err = client.Milestones.UpdateMilestone(project, milestoneID, options, gitlab.WithContext(ctx))
148+
if err != nil {
149+
log.Printf("[WARN] failed to update gitlab milestone in project %s with ID %d", project, milestoneID)
150+
return diag.FromErr(err)
151+
}
152+
153+
return resourceGitlabProjectMilestoneRead(ctx, d, meta)
154+
}
155+
156+
func resourceGitlabProjectMilestoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
157+
client := meta.(*gitlab.Client)
158+
project, milestoneID, err := resourceGitLabProjectIssueParseId(d.Id())
159+
if err != nil {
160+
return diag.FromErr(err)
161+
}
162+
163+
log.Printf("[debug] delete gitlab milestone in project %s with ID %d", project, milestoneID)
164+
resp, err := client.Milestones.DeleteMilestone(project, milestoneID, gitlab.WithContext(ctx))
165+
if err != nil {
166+
log.Printf("[debug] failed to delete gitlab milestone in project %s with ID %d. Response %v", project, milestoneID, resp)
167+
return diag.FromErr(err)
168+
}
169+
return nil
170+
}
171+
172+
func resourceGitLabProjectMilestoneBuildId(project string, milestoneID int) string {
173+
stringMilestoneID := fmt.Sprintf("%d", milestoneID)
174+
return buildTwoPartID(&project, &stringMilestoneID)
175+
}
176+
177+
func resourceGitLabProjectMilestoneParseId(id string) (string, int, error) {
178+
project, milestone, err := parseTwoPartID(id)
179+
if err != nil {
180+
return "", 0, err
181+
}
182+
183+
milestoneID, err := strconv.Atoi(milestone)
184+
if err != nil {
185+
return "", 0, err
186+
}
187+
188+
return project, milestoneID, nil
189+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package provider
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"time"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
12+
gitlab "github.com/xanzy/go-gitlab"
13+
)
14+
15+
func TestAccGitlabProjectMilestone_basic(t *testing.T) {
16+
testAccCheck(t)
17+
18+
var milestone gitlab.Milestone
19+
var milestoneUpdate gitlab.Milestone
20+
rInt1, rInt2 := acctest.RandInt(), acctest.RandInt()
21+
project := testAccCreateProject(t)
22+
23+
resource.Test(t, resource.TestCase{
24+
PreCheck: func() { testAccPreCheck(t) },
25+
ProviderFactories: providerFactories,
26+
CheckDestroy: testAccCheckGitlabProjectMilestoneDestroy,
27+
Steps: []resource.TestStep{
28+
{
29+
// create Milestone with required values only
30+
Config: testAccGitlabProjectMilestoneConfigRequiredOnly(project.ID, rInt1, ""),
31+
Check: resource.ComposeTestCheckFunc(
32+
testAccCheckGitlabProjectMilestoneExists("this", &milestone),
33+
testAccCheckGitlabProjectMilestoneAttributes("this", &milestone, &testAccGitlabProjectMilestoneExpectedAttributes{
34+
Title: fmt.Sprintf("test-%d", rInt1),
35+
ProjectID: project.ID,
36+
Description: "",
37+
StartDate: gitlab.ISOTime{},
38+
DueDate: gitlab.ISOTime{},
39+
State: "active",
40+
Expired: false,
41+
}),
42+
),
43+
},
44+
{
45+
// verify import
46+
ResourceName: "gitlab_project_milestone.this",
47+
ImportState: true,
48+
ImportStateVerify: true,
49+
},
50+
{
51+
// update some Milestone attributes
52+
Config: testAccGitlabProjectMilestoneConfigAll(project.ID, rInt2, "2022-04-10", "2022-04-15", "closed"),
53+
Check: resource.ComposeTestCheckFunc(
54+
testAccCheckGitlabProjectMilestoneExists("this", &milestoneUpdate),
55+
testAccCheckGitlabProjectMilestoneAttributes("this", &milestoneUpdate, &testAccGitlabProjectMilestoneExpectedAttributes{
56+
Title: fmt.Sprintf("test-%d", rInt2),
57+
ProjectID: project.ID,
58+
Description: fmt.Sprintf("test-%d", rInt2),
59+
StartDate: gitlab.ISOTime(time.Date(2022, time.April, 10, 0, 0, 0, 0, time.UTC)),
60+
DueDate: gitlab.ISOTime(time.Date(2022, time.April, 15, 0, 0, 0, 0, time.UTC)),
61+
State: "closed",
62+
Expired: true,
63+
}),
64+
),
65+
},
66+
},
67+
})
68+
}
69+
70+
func testAccCheckGitlabProjectMilestoneDestroy(s *terraform.State) error {
71+
for _, rs := range s.RootModule().Resources {
72+
if rs.Type != "gitlab_project_milestone" {
73+
continue
74+
}
75+
project, milestoneID, err := resourceGitLabProjectMilestoneParseId(rs.Primary.ID)
76+
if err != nil {
77+
return err
78+
}
79+
80+
milestone, _, err := testGitlabClient.Milestones.GetMilestone(project, milestoneID)
81+
if err == nil && milestone != nil {
82+
return errors.New("Milestone still exists")
83+
}
84+
if !is404(err) {
85+
return err
86+
}
87+
return nil
88+
}
89+
return nil
90+
}
91+
92+
func testAccCheckGitlabProjectMilestoneAttributes(n string, milestone *gitlab.Milestone, want *testAccGitlabProjectMilestoneExpectedAttributes) resource.TestCheckFunc {
93+
return func(s *terraform.State) error {
94+
if milestone.Title != want.Title {
95+
return fmt.Errorf("Got milestone title '%s'; want '%s'", milestone.Title, want.Title)
96+
}
97+
if milestone.ProjectID != want.ProjectID {
98+
return fmt.Errorf("Got milestone project_id '%d'; want '%d'", milestone.ProjectID, want.ProjectID)
99+
}
100+
if milestone.Description != want.Description {
101+
return fmt.Errorf("Got milestone description '%s'; want '%s'", milestone.Description, want.Description)
102+
}
103+
startDate := gitlab.ISOTime(time.Date(0001, time.January, 1, 0, 0, 0, 0, time.UTC))
104+
if milestone.StartDate != nil {
105+
startDate = *milestone.StartDate
106+
}
107+
if startDate != want.StartDate {
108+
return fmt.Errorf("Got milestone start_date '%s'; want '%s'", milestone.StartDate, want.StartDate)
109+
}
110+
dueDate := gitlab.ISOTime(time.Date(0001, time.January, 1, 0, 0, 0, 0, time.UTC))
111+
if milestone.DueDate != nil {
112+
dueDate = *milestone.DueDate
113+
}
114+
if dueDate != want.DueDate {
115+
return fmt.Errorf("Got milestone due_date '%s'; want '%s'", milestone.DueDate, want.DueDate)
116+
}
117+
if milestone.State != want.State {
118+
return fmt.Errorf("Got milestone state '%s'; want '%s'", milestone.State, want.State)
119+
}
120+
expired := false
121+
if milestone.Expired != nil {
122+
expired = *milestone.Expired
123+
}
124+
if expired != want.Expired {
125+
return fmt.Errorf("Got milestone expired '%v'; want '%v'", milestone.Expired, want.Expired)
126+
}
127+
return nil
128+
}
129+
}
130+
131+
func testAccCheckGitlabProjectMilestoneExists(n string, milestone *gitlab.Milestone) resource.TestCheckFunc {
132+
return func(s *terraform.State) error {
133+
rs, ok := s.RootModule().Resources[fmt.Sprintf("gitlab_project_milestone.%s", n)]
134+
if !ok {
135+
return fmt.Errorf("Not Found: %s", n)
136+
}
137+
project, milestoneID, err := resourceGitLabProjectMilestoneParseId(rs.Primary.ID)
138+
if err != nil {
139+
return fmt.Errorf("Error in splitting project and milestoneID")
140+
}
141+
gotMilestone, _, err := testGitlabClient.Milestones.GetMilestone(project, milestoneID)
142+
if err != nil {
143+
return err
144+
}
145+
*milestone = *gotMilestone
146+
return err
147+
}
148+
}
149+
150+
func testAccGitlabProjectMilestoneConfigRequiredOnly(project int, rInt int, additinalOptions string) string {
151+
return fmt.Sprintf(`
152+
resource "gitlab_project_milestone" "this" {
153+
project_id = "%d"
154+
title = "test-%d"
155+
%s
156+
}
157+
`, project, rInt, additinalOptions)
158+
}
159+
160+
func testAccGitlabProjectMilestoneConfigAll(project int, rInt int, startDate string, dueDate string, state string) string {
161+
additinalOptions := fmt.Sprintf(`
162+
description = "test-%d"
163+
start_date = "%s"
164+
due_date = "%s"
165+
state = "%s"
166+
`, rInt, startDate, dueDate, state)
167+
return testAccGitlabProjectMilestoneConfigRequiredOnly(project, rInt, additinalOptions)
168+
}
169+
170+
type testAccGitlabProjectMilestoneExpectedAttributes struct {
171+
Title string
172+
ProjectID int
173+
Description string
174+
StartDate gitlab.ISOTime
175+
DueDate gitlab.ISOTime
176+
State string
177+
Expired bool
178+
}

0 commit comments

Comments
 (0)