Skip to content

Commit 7a1b52b

Browse files
authored
add bitbucket support (#260)
* add bitbucket support * add unit tests for repo URL expanding for Github + Bitbucket * Unit Tests, Service Connection validation by @nmiodice * fix validateServiceConnectionIDExistsIfNeeded
1 parent 80ef658 commit 7a1b52b

File tree

4 files changed

+199
-21
lines changed

4 files changed

+199
-21
lines changed

azuredevops/resource_build_definition.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package azuredevops
22

33
import (
4+
"errors"
45
"fmt"
56
"strconv"
67
"strings"
@@ -122,7 +123,7 @@ func resourceBuildDefinition() *schema.Resource {
122123
"repo_type": {
123124
Type: schema.TypeString,
124125
Required: true,
125-
ValidateFunc: validation.StringInSlice([]string{"GitHub", "TfsGit"}, false),
126+
ValidateFunc: validation.StringInSlice([]string{"GitHub", "TfsGit", "Bitbucket"}, false),
126127
},
127128
"branch_name": {
128129
Type: schema.TypeString,
@@ -250,6 +251,10 @@ func resourceBuildDefinition() *schema.Resource {
250251

251252
func resourceBuildDefinitionCreate(d *schema.ResourceData, m interface{}) error {
252253
clients := m.(*config.AggregatedClient)
254+
err := validateServiceConnectionIDExistsIfNeeded(d)
255+
if err != nil {
256+
return err
257+
}
253258
buildDefinition, projectID, err := expandBuildDefinition(d)
254259
if err != nil {
255260
return fmt.Errorf("Error creating resource Build Definition: %+v", err)
@@ -346,6 +351,10 @@ func resourceBuildDefinitionDelete(d *schema.ResourceData, m interface{}) error
346351

347352
func resourceBuildDefinitionUpdate(d *schema.ResourceData, m interface{}) error {
348353
clients := m.(*config.AggregatedClient)
354+
err := validateServiceConnectionIDExistsIfNeeded(d)
355+
if err != nil {
356+
return err
357+
}
349358
buildDefinition, projectID, err := expandBuildDefinition(d)
350359
if err != nil {
351360
return err
@@ -706,6 +715,9 @@ func expandBuildDefinition(d *schema.ResourceData) (*build.BuildDefinition, stri
706715
if strings.EqualFold(repoType, "github") {
707716
repoURL = fmt.Sprintf("https://github.com/%s.git", repoName)
708717
}
718+
if strings.EqualFold(repoType, "bitbucket") {
719+
repoURL = fmt.Sprintf("https://bitbucket.org/%s.git", repoName)
720+
}
709721

710722
ciTriggers := expandBuildDefinitionTriggerList(
711723
d.Get("ci_trigger").([]interface{}),
@@ -763,6 +775,23 @@ func expandBuildDefinition(d *schema.ResourceData) (*build.BuildDefinition, stri
763775
return &buildDefinition, projectID, nil
764776
}
765777

778+
/**
779+
* certain types of build definitions require a service connection to run. This function
780+
* returns an error if a service connection was needed but not provided
781+
*/
782+
func validateServiceConnectionIDExistsIfNeeded(d *schema.ResourceData) error {
783+
repositories := d.Get("repository").([]interface{})
784+
repository := repositories[0].(map[string]interface{})
785+
786+
repoType := repository["repo_type"].(string)
787+
serviceConnectionID := repository["service_connection_id"].(string)
788+
789+
if strings.EqualFold(repoType, "bitbucket") && serviceConnectionID == "" {
790+
return errors.New("bitbucket repositories need a referenced service connection ID")
791+
}
792+
return nil
793+
}
794+
766795
func buildVariableGroup(id int) *build.VariableGroup {
767796
return &build.VariableGroup{
768797
Id: &id,

azuredevops/resource_build_definition_test.go

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"regexp"
910
"sort"
1011
"strconv"
1112
"testing"
@@ -126,13 +127,47 @@ var testBuildDefinition = build.BuildDefinition{
126127
VariableGroups: &[]build.VariableGroup{},
127128
}
128129

130+
// This definition matches the overall structure of what a configured Bitbucket git repository would
131+
// look like.
132+
func testBuildDefinitionBitbucket() build.BuildDefinition {
133+
return build.BuildDefinition{
134+
Id: converter.Int(100),
135+
Revision: converter.Int(1),
136+
Name: converter.String("Name"),
137+
Path: converter.String("\\"),
138+
Repository: &build.BuildRepository{
139+
Url: converter.String("https://bitbucket.com/RepoId.git"),
140+
Id: converter.String("RepoId"),
141+
Name: converter.String("RepoId"),
142+
DefaultBranch: converter.String("RepoBranchName"),
143+
Type: converter.String("Bitbucket"),
144+
Properties: &map[string]string{
145+
"connectedServiceId": "ServiceConnectionID",
146+
},
147+
},
148+
Process: &build.YamlProcess{
149+
YamlFilename: converter.String("YamlFilename"),
150+
},
151+
Queue: &build.AgentPoolQueue{
152+
Name: converter.String("BuildPoolName"),
153+
Pool: &build.TaskAgentPoolReference{
154+
Name: converter.String("BuildPoolName"),
155+
},
156+
},
157+
QueueStatus: &build.DefinitionQueueStatusValues.Enabled,
158+
Type: &build.DefinitionTypeValues.Build,
159+
Quality: &build.DefinitionQualityValues.Definition,
160+
VariableGroups: &[]build.VariableGroup{},
161+
}
162+
}
163+
129164
/**
130165
* Begin unit tests
131166
*/
132167

133168
// validates that all supported repo types are allowed by the schema
134169
func TestAzureDevOpsBuildDefinition_RepoTypeListIsCorrect(t *testing.T) {
135-
expectedRepoTypes := []string{"GitHub", "TfsGit"}
170+
expectedRepoTypes := []string{"GitHub", "TfsGit", "Bitbucket"}
136171
repoSchema := resourceBuildDefinition().Schema["repository"]
137172
repoTypeSchema := repoSchema.Elem.(*schema.Resource).Schema["repo_type"]
138173

@@ -160,6 +195,50 @@ func TestAzureDevOpsBuildDefinition_PathInvalidStartingSlashIsError(t *testing.T
160195
require.Equal(t, "path must start with backslash", errors[0].Error())
161196
}
162197

198+
// verifies that GitHub repo urls are expanded to URLs Azure DevOps expects
199+
func TestAzureDevOpsBuildDefinition_Expand_RepoUrl_Github(t *testing.T) {
200+
resourceData := schema.TestResourceDataRaw(t, resourceBuildDefinition().Schema, nil)
201+
flattenBuildDefinition(resourceData, &testBuildDefinition, testProjectID)
202+
buildDefinitionAfterRoundTrip, projectID, err := expandBuildDefinition(resourceData)
203+
204+
require.Nil(t, err)
205+
require.Equal(t, *buildDefinitionAfterRoundTrip.Repository.Url, "https://github.com/RepoId.git")
206+
require.Equal(t, testProjectID, projectID)
207+
}
208+
209+
// verifies that Bitbucket repo urls are expanded to URLs Azure DevOps expects
210+
func TestAzureDevOpsBuildDefinition_Expand_RepoUrl_Bitbucket(t *testing.T) {
211+
resourceData := schema.TestResourceDataRaw(t, resourceBuildDefinition().Schema, nil)
212+
bitBucketBuildDef := testBuildDefinitionBitbucket()
213+
flattenBuildDefinition(resourceData, &bitBucketBuildDef, testProjectID)
214+
buildDefinitionAfterRoundTrip, projectID, err := expandBuildDefinition(resourceData)
215+
216+
require.Nil(t, err)
217+
require.Equal(t, *buildDefinitionAfterRoundTrip.Repository.Url, "https://bitbucket.org/RepoId.git")
218+
require.Equal(t, testProjectID, projectID)
219+
}
220+
221+
// verifies that a service connection is required for bitbucket repos
222+
func TestAzureDevOpsBuildDefinition_ValidatesServiceConnection_Bitbucket(t *testing.T) {
223+
resourceData := schema.TestResourceDataRaw(t, resourceBuildDefinition().Schema, nil)
224+
bitBucketBuildDef := testBuildDefinitionBitbucket()
225+
(*bitBucketBuildDef.Repository.Properties)["connectedServiceId"] = ""
226+
flattenBuildDefinition(resourceData, &bitBucketBuildDef, testProjectID)
227+
228+
ctrl := gomock.NewController(t)
229+
defer ctrl.Finish()
230+
buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
231+
clients := &config.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
232+
233+
err := resourceBuildDefinitionCreate(resourceData, clients)
234+
require.NotNil(t, err)
235+
require.Contains(t, err.Error(), "bitbucket repositories need a referenced service connection ID")
236+
237+
err = resourceBuildDefinitionUpdate(resourceData, clients)
238+
require.NotNil(t, err)
239+
require.Contains(t, err.Error(), "bitbucket repositories need a referenced service connection ID")
240+
}
241+
163242
// verifies that the flatten/expand round trip yields the same build definition
164243
func TestAzureDevOpsBuildDefinition_ExpandFlatten_Roundtrip(t *testing.T) {
165244
resourceData := schema.TestResourceDataRaw(t, resourceBuildDefinition().Schema, nil)
@@ -281,7 +360,7 @@ func TestAzureDevOpsBuildDefinition_Update_DoesNotSwallowError(t *testing.T) {
281360

282361
// validates that an apply followed by another apply (i.e., resource update) will be reflected in AzDO and the
283362
// underlying terraform state.
284-
func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
363+
func TestAccAzureDevOpsBuildDefinition_Create_Update_Import(t *testing.T) {
285364
projectName := testhelper.TestAccResourcePrefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
286365
buildDefinitionPathEmpty := `\`
287366
buildDefinitionNameFirst := testhelper.TestAccResourcePrefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
@@ -300,7 +379,7 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
300379
CheckDestroy: testAccBuildDefinitionCheckDestroy,
301380
Steps: []resource.TestStep{
302381
{
303-
Config: testhelper.TestAccBuildDefinitionResource(projectName, buildDefinitionNameFirst, buildDefinitionPathEmpty),
382+
Config: testhelper.TestAccBuildDefinitionResourceGitHub(projectName, buildDefinitionNameFirst, buildDefinitionPathEmpty),
304383
Check: resource.ComposeTestCheckFunc(
305384
testAccCheckBuildDefinitionResourceExists(buildDefinitionNameFirst),
306385
resource.TestCheckResourceAttrSet(tfBuildDefNode, "project_id"),
@@ -309,7 +388,7 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
309388
resource.TestCheckResourceAttr(tfBuildDefNode, "path", buildDefinitionPathEmpty),
310389
),
311390
}, {
312-
Config: testhelper.TestAccBuildDefinitionResource(projectName, buildDefinitionNameSecond, buildDefinitionPathEmpty),
391+
Config: testhelper.TestAccBuildDefinitionResourceGitHub(projectName, buildDefinitionNameSecond, buildDefinitionPathEmpty),
313392
Check: resource.ComposeTestCheckFunc(
314393
testAccCheckBuildDefinitionResourceExists(buildDefinitionNameSecond),
315394
resource.TestCheckResourceAttrSet(tfBuildDefNode, "project_id"),
@@ -318,7 +397,7 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
318397
resource.TestCheckResourceAttr(tfBuildDefNode, "path", buildDefinitionPathEmpty),
319398
),
320399
}, {
321-
Config: testhelper.TestAccBuildDefinitionResource(projectName, buildDefinitionNameFirst, buildDefinitionPathFirst),
400+
Config: testhelper.TestAccBuildDefinitionResourceGitHub(projectName, buildDefinitionNameFirst, buildDefinitionPathFirst),
322401
Check: resource.ComposeTestCheckFunc(
323402
testAccCheckBuildDefinitionResourceExists(buildDefinitionNameFirst),
324403
resource.TestCheckResourceAttrSet(tfBuildDefNode, "project_id"),
@@ -327,7 +406,7 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
327406
resource.TestCheckResourceAttr(tfBuildDefNode, "path", buildDefinitionPathFirst),
328407
),
329408
}, {
330-
Config: testhelper.TestAccBuildDefinitionResource(projectName, buildDefinitionNameFirst,
409+
Config: testhelper.TestAccBuildDefinitionResourceGitHub(projectName, buildDefinitionNameFirst,
331410
buildDefinitionPathSecond),
332411
Check: resource.ComposeTestCheckFunc(
333412
testAccCheckBuildDefinitionResourceExists(buildDefinitionNameFirst),
@@ -337,7 +416,7 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
337416
resource.TestCheckResourceAttr(tfBuildDefNode, "path", buildDefinitionPathSecond),
338417
),
339418
}, {
340-
Config: testhelper.TestAccBuildDefinitionResource(projectName, buildDefinitionNameFirst, buildDefinitionPathThird),
419+
Config: testhelper.TestAccBuildDefinitionResourceGitHub(projectName, buildDefinitionNameFirst, buildDefinitionPathThird),
341420
Check: resource.ComposeTestCheckFunc(
342421
testAccCheckBuildDefinitionResourceExists(buildDefinitionNameFirst),
343422
resource.TestCheckResourceAttrSet(tfBuildDefNode, "project_id"),
@@ -346,7 +425,7 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
346425
resource.TestCheckResourceAttr(tfBuildDefNode, "path", buildDefinitionPathThird),
347426
),
348427
}, {
349-
Config: testhelper.TestAccBuildDefinitionResource(projectName, buildDefinitionNameFirst, buildDefinitionPathFourth),
428+
Config: testhelper.TestAccBuildDefinitionResourceGitHub(projectName, buildDefinitionNameFirst, buildDefinitionPathFourth),
350429
Check: resource.ComposeTestCheckFunc(
351430
testAccCheckBuildDefinitionResourceExists(buildDefinitionNameFirst),
352431
resource.TestCheckResourceAttrSet(tfBuildDefNode, "project_id"),
@@ -365,6 +444,25 @@ func TestAccAzureDevOpsBuildDefinition_CreateAndUpdate(t *testing.T) {
365444
})
366445
}
367446

447+
// Verifies a build for Bitbucket can happen. Note: the update/import logic is tested in other tests
448+
func TestAccAzureDevOpsBuildDefinitionBitbucket_Create(t *testing.T) {
449+
projectName := testhelper.TestAccResourcePrefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
450+
resource.Test(t, resource.TestCase{
451+
PreCheck: func() { testhelper.TestAccPreCheck(t, nil) },
452+
Providers: testAccProviders,
453+
CheckDestroy: testAccBuildDefinitionCheckDestroy,
454+
Steps: []resource.TestStep{
455+
{
456+
Config: testhelper.TestAccBuildDefinitionResourceBitbucket(projectName, "build-def-name", "\\", ""),
457+
ExpectError: regexp.MustCompile("bitbucket repositories need a referenced service connection ID"),
458+
}, {
459+
Config: testhelper.TestAccBuildDefinitionResourceBitbucket(projectName, "build-def-name", "\\", "some-service-connection"),
460+
Check: testAccCheckBuildDefinitionResourceExists("build-def-name"),
461+
},
462+
},
463+
})
464+
}
465+
368466
// Given the name of an AzDO build definition, this will return a function that will check whether
369467
// or not the definition (1) exists in the state and (2) exist in AzDO and (3) has the correct name
370468
func testAccCheckBuildDefinitionResourceExists(expectedName string) resource.TestCheckFunc {

azuredevops/utils/testhelper/hcl.go

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,22 +173,61 @@ resource "azuredevops_agent_pool" "pool" {
173173
}`, poolName)
174174
}
175175

176+
// TestAccBuildDefinitionResourceGitHub HCL describing an AzDO build definition sourced from GitHub
177+
func TestAccBuildDefinitionResourceGitHub(projectName string, buildDefinitionName string, buildPath string) string {
178+
return TestAccBuildDefinitionResource(
179+
projectName,
180+
buildDefinitionName,
181+
buildPath,
182+
"GitHub",
183+
"repoOrg/repoName",
184+
"master",
185+
"path/to/yaml",
186+
"")
187+
}
188+
189+
// TestAccBuildDefinitionResourceBitbucket HCL describing an AzDO build definition sourced from Bitbucket
190+
func TestAccBuildDefinitionResourceBitbucket(projectName string, buildDefinitionName string, buildPath string, serviceConnectionID string) string {
191+
return TestAccBuildDefinitionResource(
192+
projectName,
193+
buildDefinitionName,
194+
buildPath,
195+
"Bitbucket",
196+
"repoOrg/repoName",
197+
"master",
198+
"path/to/yaml",
199+
serviceConnectionID)
200+
}
201+
176202
// TestAccBuildDefinitionResource HCL describing an AzDO build definition
177-
func TestAccBuildDefinitionResource(projectName string, buildDefinitionName string, buildPath string) string {
203+
func TestAccBuildDefinitionResource(
204+
projectName string,
205+
buildDefinitionName string,
206+
buildPath string,
207+
repoType string,
208+
repoName string,
209+
branchName string,
210+
yamlPath string,
211+
serviceConnectionID string,
212+
) string {
213+
repositoryBlock := fmt.Sprintf(`
214+
repository {
215+
repo_type = "%s"
216+
repo_name = "%s"
217+
branch_name = "%s"
218+
yml_path = "%s"
219+
service_connection_id = "%s"
220+
}`, repoType, repoName, branchName, yamlPath, serviceConnectionID)
221+
178222
buildDefinitionResource := fmt.Sprintf(`
179223
resource "azuredevops_build_definition" "build" {
180224
project_id = azuredevops_project.project.id
181225
name = "%s"
182226
agent_pool_name = "Hosted Ubuntu 1604"
183227
path = "%s"
184228
185-
repository {
186-
repo_type = "GitHub"
187-
repo_name = "repoOrg/repoName"
188-
branch_name = "branch"
189-
yml_path = "path/to/yaml"
190-
}
191-
}`, buildDefinitionName, strings.ReplaceAll(buildPath, `\`, `\\`))
229+
%s
230+
}`, buildDefinitionName, strings.ReplaceAll(buildPath, `\`, `\\`), repositoryBlock)
192231

193232
projectResource := TestAccProjectResource(projectName)
194233
return fmt.Sprintf("%s\n%s", projectResource, buildDefinitionResource)

website/docs/r/build_definition.html.markdown

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ resource "azuredevops_git_repository" "repository" {
3939
}
4040
}
4141
42+
resource "azuredevops_variable_group" "vars" {
43+
project_id = local.project_id
44+
name = "Infrastructure Pipeline Variables"
45+
description = "Managed by Terraform"
46+
allow_access = true
47+
48+
variable {
49+
name = "FOO"
50+
value = "BAR"
51+
}
52+
}
53+
4254
resource "azuredevops_build_definition" "build" {
4355
project_id = azuredevops_project.project.id
4456
name = "Sample Build Definition"
@@ -63,9 +75,9 @@ resource "azuredevops_build_definition" "build" {
6375
yml_path = "azure-pipelines.yml"
6476
}
6577
66-
# Until https://github.com/microsoft/terraform-provider-azuredevops/issues/170, these are assumed
67-
# to already exist in the project.
68-
variables_groups = [1, 2, 3]
78+
variable_groups = [
79+
azuredevops_variable_group.vars.id
80+
]
6981
}
7082
```
7183

@@ -85,7 +97,7 @@ The following arguments are supported:
8597

8698
* `branch_name` - (Optional) The branch name for which builds are triggered. Defaults to `master`.
8799
* `repo_name` - (Required) The name of the repository.
88-
* `repo_type` - (Optional) The repository type. Valid values: `GitHub` or `TfsGit`. Defaults to `Github`.
100+
* `repo_type` - (Optional) The repository type. Valid values: `GitHub` or `TfsGit` or `Bitbucket`. Defaults to `Github`.
89101
* `service_connection_id` - (Optional) The service connection ID. Used if the `repo_type` is `GitHub`.
90102
* `yml_path` - (Required) The path of the Yaml file describing the build definition.
91103

0 commit comments

Comments
 (0)