Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for GitHub enterprise as a build definition repository type #97

Merged
merged 3 commits into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions azuredevops/internal/model/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ package model
type RepoType string

type repoTypeValuesType struct {
GitHub RepoType
TfsGit RepoType
Bitbucket RepoType
GitHub RepoType
TfsGit RepoType
Bitbucket RepoType
GitHubEnterprise RepoType
}

// RepoTypeValues enum of the type of the repository
var RepoTypeValues = repoTypeValuesType{
GitHub: "GitHub",
TfsGit: "TfsGit",
Bitbucket: "Bitbucket",
GitHub: "GitHub",
TfsGit: "TfsGit",
Bitbucket: "Bitbucket",
GitHubEnterprise: "GitHubEnterprise",
}
27 changes: 26 additions & 1 deletion azuredevops/internal/service/build/resource_build_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package build
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"

Expand Down Expand Up @@ -161,6 +162,7 @@ func ResourceBuildDefinition() *schema.Resource {
string(model.RepoTypeValues.GitHub),
string(model.RepoTypeValues.TfsGit),
string(model.RepoTypeValues.Bitbucket),
string(model.RepoTypeValues.GitHubEnterprise),
}, false),
},
"branch_name": {
Expand All @@ -173,6 +175,11 @@ func ResourceBuildDefinition() *schema.Resource {
Optional: true,
Default: "",
},
"github_enterprise_url": {
Type: schema.TypeString,
Optional: true,
Default: "",
},
},
},
},
Expand Down Expand Up @@ -458,24 +465,34 @@ func flattenVariableGroups(buildDefinition *build.BuildDefinition) []int {

func flattenRepository(buildDefinition *build.BuildDefinition) interface{} {
yamlFilePath := ""
githubEnterpriseUrl := ""

// The process member can be of many types -- the only typing information
// available from the compiler is `interface{}` so we can probe for known
// implementations
if processMap, ok := buildDefinition.Process.(map[string]interface{}); ok {
yamlFilePath = processMap["yamlFilename"].(string)
}

if yamlProcess, ok := buildDefinition.Process.(*build.YamlProcess); ok {
yamlFilePath = *yamlProcess.YamlFilename
}

// Set github_enterprise_url value from buildDefinition.Repository URL
if strings.EqualFold(*buildDefinition.Repository.Type, string(model.RepoTypeValues.GitHubEnterprise)) {
url, err := url.Parse(*buildDefinition.Repository.Url)
if err != nil {
return fmt.Errorf("Unable to parse repository URL")
}
githubEnterpriseUrl = fmt.Sprintf("%s://%s", url.Scheme, url.Host)
}

return []map[string]interface{}{{
"yml_path": yamlFilePath,
"repo_id": *buildDefinition.Repository.Id,
"repo_type": *buildDefinition.Repository.Type,
"branch_name": *buildDefinition.Repository.DefaultBranch,
"service_connection_id": (*buildDefinition.Repository.Properties)["connectedServiceId"],
"github_enterprise_url": githubEnterpriseUrl,
}}
}

Expand Down Expand Up @@ -831,6 +848,11 @@ func expandBuildDefinition(d *schema.ResourceData) (*build.BuildDefinition, stri
repoURL = fmt.Sprintf("https://bitbucket.org/%s.git", repoID)
repoAPIURL = fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s", repoID)
}
if strings.EqualFold(string(repoType), string(model.RepoTypeValues.GitHubEnterprise)) {
githubEnterpriseURL := repository["github_enterprise_url"].(string)
repoURL = fmt.Sprintf("%s/%s.git", githubEnterpriseURL, repoID)
repoAPIURL = fmt.Sprintf("%s/api/v3/repos/%s", githubEnterpriseURL, repoID)
}

ciTriggers := expandBuildDefinitionTriggerList(
d.Get("ci_trigger").([]interface{}),
Expand Down Expand Up @@ -909,6 +931,9 @@ func validateServiceConnectionIDExistsIfNeeded(d *schema.ResourceData) error {
if strings.EqualFold(repoType, string(model.RepoTypeValues.Bitbucket)) && serviceConnectionID == "" {
return errors.New("bitbucket repositories need a referenced service connection ID")
}
if strings.EqualFold(repoType, string(model.RepoTypeValues.GitHubEnterprise)) && serviceConnectionID == "" {
return errors.New("GitHub Enterprise repositories need a referenced service connection ID")
}
return nil
}

Expand Down
130 changes: 129 additions & 1 deletion azuredevops/internal/service/build/resource_build_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,43 @@ var testBuildDefinitionBitbucketWithCITrigger = build.BuildDefinition{
VariableGroups: &[]build.VariableGroup{},
}

// This definition matches the overall structure of what a configured GitHub Enterprise git repository would
// look like. Note that the ID and Name attributes match -- this is the service-side behavior
// when configuring a GitHub Enterprise repo.
var testBuildDefinitionGitHubEnterpriseWithCITrigger = build.BuildDefinition{
Id: converter.Int(100),
Revision: converter.Int(1),
Name: converter.String("Name"),
Path: converter.String("\\"),
Repository: &build.BuildRepository{
Url: converter.String("https://github.company.com/RepoId.git"),
Id: converter.String("RepoId"),
Name: converter.String("RepoId"),
DefaultBranch: converter.String("RepoBranchName"),
Type: converter.String("GitHubEnterprise"),
Properties: &map[string]string{
"connectedServiceId": "ServiceConnectionID",
"apiUrl": "https://github.company.com/api/v3/repos/RepoId",
},
},
Triggers: &[]interface{}{
yamlCiTrigger,
},
Process: &build.YamlProcess{
YamlFilename: converter.String("YamlFilename"),
},
Queue: &build.AgentPoolQueue{
Name: converter.String("BuildPoolName"),
Pool: &build.TaskAgentPoolReference{
Name: converter.String("BuildPoolName"),
},
},
QueueStatus: &build.DefinitionQueueStatusValues.Enabled,
Type: &build.DefinitionTypeValues.Build,
Quality: &build.DefinitionQualityValues.Definition,
VariableGroups: &[]build.VariableGroup{},
}

// This definition matches the overall structure of what a configured Bitbucket git repository would
// look like.
func testBuildDefinitionBitbucket() build.BuildDefinition {
Expand Down Expand Up @@ -192,9 +229,44 @@ func testBuildDefinitionBitbucket() build.BuildDefinition {
}
}

// This definition matches the overall structure of what a configured Github Enterprise git repository would
// look like.
func testBuildDefinitionGitHubEnterprise() build.BuildDefinition {
return build.BuildDefinition{
Id: converter.Int(100),
Revision: converter.Int(1),
Name: converter.String("Name"),
Path: converter.String("\\"),
Repository: &build.BuildRepository{
Url: converter.String("https://github.company.com/RepoId.git"),
Id: converter.String("RepoId"),
Name: converter.String("RepoId"),
DefaultBranch: converter.String("RepoBranchName"),
Type: converter.String("GitHubEnterprise"),
Properties: &map[string]string{
"connectedServiceId": "ServiceConnectionID",
"apiUrl": "https://github.company.com/api/v3/repos/RepoId",
},
},
Process: &build.YamlProcess{
YamlFilename: converter.String("YamlFilename"),
},
Queue: &build.AgentPoolQueue{
Name: converter.String("BuildPoolName"),
Pool: &build.TaskAgentPoolReference{
Name: converter.String("BuildPoolName"),
},
},
QueueStatus: &build.DefinitionQueueStatusValues.Enabled,
Type: &build.DefinitionTypeValues.Build,
Quality: &build.DefinitionQualityValues.Definition,
VariableGroups: &[]build.VariableGroup{},
}
}

// validates that all supported repo types are allowed by the schema
func TestBuildDefinition_RepoTypeListIsCorrect(t *testing.T) {
expectedRepoTypes := []string{"GitHub", "TfsGit", "Bitbucket"}
expectedRepoTypes := []string{"GitHub", "TfsGit", "Bitbucket", "GitHubEnterprise"}
repoSchema := ResourceBuildDefinition().Schema["repository"]
repoTypeSchema := repoSchema.Elem.(*schema.Resource).Schema["repo_type"]

Expand Down Expand Up @@ -245,6 +317,19 @@ func TestBuildDefinition_Expand_RepoUrl_Bitbucket(t *testing.T) {
require.Equal(t, testProjectID, projectID)
}

// verifies that GitHub Enterprise repo urls are expanded to URLs Azure DevOps expects
func TestBuildDefinition_Expand_RepoUrl_GithubEnterprise(t *testing.T) {
resourceData := schema.TestResourceDataRaw(t, ResourceBuildDefinition().Schema, nil)
gitHubEnterpriseBuildDef := testBuildDefinitionGitHubEnterprise()

flattenBuildDefinition(resourceData, &gitHubEnterpriseBuildDef, testProjectID)
buildDefinitionAfterRoundTrip, projectID, err := expandBuildDefinition(resourceData)

require.Nil(t, err)
require.Equal(t, *buildDefinitionAfterRoundTrip.Repository.Url, "https://github.company.com/RepoId.git")
require.Equal(t, testProjectID, projectID)
}

// verifies that a service connection is required for bitbucket repos
func TestBuildDefinition_ValidatesServiceConnection_Bitbucket(t *testing.T) {
resourceData := schema.TestResourceDataRaw(t, ResourceBuildDefinition().Schema, nil)
Expand All @@ -266,6 +351,27 @@ func TestBuildDefinition_ValidatesServiceConnection_Bitbucket(t *testing.T) {
require.Contains(t, err.Error(), "bitbucket repositories need a referenced service connection ID")
}

// verifies that a service connection is required for GitHub Enterprise repos
func TestBuildDefinition_ValidatesServiceConnection_GitHubEnterprise(t *testing.T) {
resourceData := schema.TestResourceDataRaw(t, ResourceBuildDefinition().Schema, nil)
gitHubEnterpriseBuildDef := testBuildDefinitionGitHubEnterprise()
(*gitHubEnterpriseBuildDef.Repository.Properties)["connectedServiceId"] = ""
flattenBuildDefinition(resourceData, &gitHubEnterpriseBuildDef, testProjectID)

ctrl := gomock.NewController(t)
defer ctrl.Finish()
buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
clients := &client.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}

err := resourceBuildDefinitionCreate(resourceData, clients)
require.NotNil(t, err)
require.Contains(t, err.Error(), "GitHub Enterprise repositories need a referenced service connection ID")

err = resourceBuildDefinitionUpdate(resourceData, clients)
require.NotNil(t, err)
require.Contains(t, err.Error(), "GitHub Enterprise repositories need a referenced service connection ID")
}

// verifies that create a build definition with Bitbucket and CI triggers
func TestAzureDevOpsBuildDefinition_CITriggers_Bitbucket(t *testing.T) {
ctrl := gomock.NewController(t)
Expand All @@ -288,6 +394,28 @@ func TestAzureDevOpsBuildDefinition_CITriggers_Bitbucket(t *testing.T) {
require.Contains(t, err.Error(), "CreateDefinition() Failed")
}

// verifies that create a build definition with GitHub Enterprise and CI triggers
func TestAzureDevOpsBuildDefinition_CITriggers_GitHubEnterprise(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

resourceData := schema.TestResourceDataRaw(t, ResourceBuildDefinition().Schema, nil)
flattenBuildDefinition(resourceData, &testBuildDefinitionGitHubEnterpriseWithCITrigger, testProjectID)

buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
clients := &client.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}

expectedArgs := build.CreateDefinitionArgs{Definition: &testBuildDefinitionGitHubEnterpriseWithCITrigger, Project: &testProjectID}

buildClient.
EXPECT().
CreateDefinition(clients.Ctx, expectedArgs).
Return(nil, errors.New("CreateDefinition() Failed")).
Times(1)
err := resourceBuildDefinitionCreate(resourceData, clients)
require.Contains(t, err.Error(), "CreateDefinition() Failed")
}

// verifies that the flatten/expand round trip yields the same build definition
func TestBuildDefinition_ExpandFlatten_Roundtrip(t *testing.T) {
resourceData := schema.TestResourceDataRaw(t, ResourceBuildDefinition().Schema, nil)
Expand Down
29 changes: 27 additions & 2 deletions website/docs/r/build_definition.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Manages a Build Definition within Azure DevOps.

## Example Usage

### Tfs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does Tfs mean ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Team Foundation Server + Git. It's an example when repo_type is TfsGit

```hcl
resource "azuredevops_project" "project" {
project_name = "Sample Project"
Expand Down Expand Up @@ -72,6 +73,29 @@ resource "azuredevops_build_definition" "build" {
}
```

### GitHub Enterprise
```hcl
resource "azuredevops_build_definition" "sample_dotnetcore_app_release" {
project_id = azuredevops_project.project.id
name = "Sample Build Definition"
path = "\\ExampleFolder"

ci_trigger {
use_yaml = true
}

repository {
repo_type = "GitHubEnterprise"
repo_id = "<GitHub Org>/<Repo Name>"
github_enterprise_url = "https://github.company.com"
branch_name = "master"
yml_path = "azure-pipelines.yml"
service_connection_id = "..."
}

}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -98,9 +122,10 @@ The following arguments are supported:

- `branch_name` - (Optional) The branch name for which builds are triggered. Defaults to `master`.
- `repo_id` - (Required) The id of the repository. For `TfsGit` repos, this is simply the ID of the repository. For `Github` repos, this will take the form of `<GitHub Org>/<Repo Name>`. For `Bitbucket` repos, this will take the form of `<Workspace ID>/<Repo Name>`.
- `repo_type` - (Optional) The repository type. Valid values: `GitHub` or `TfsGit` or `Bitbucket`. Defaults to `Github`.
- `service_connection_id` - (Optional) The service connection ID. Used if the `repo_type` is `GitHub`.
- `repo_type` - (Optional) The repository type. Valid values: `GitHub` or `TfsGit` or `Bitbucket` or `GitHub Enterprise`. Defaults to `Github`. If `repo_type` is `GitHubEnterprise`, must use existing project and GitHub Enterprise service connection.
- `service_connection_id` - (Optional) The service connection ID. Used if the `repo_type` is `GitHub` or `GitHubEnterprise`.
- `yml_path` - (Required) The path of the Yaml file describing the build definition.
- `github_enterprise_url` - (Optional) The Github Enterprise URL. Used if `repo_type` is `GithubEnterprise`.

`ci_trigger` block supports the following:

Expand Down