diff --git a/azuredevops/crud/serviceendpoint/crud_service_endpoint.go b/azuredevops/crud/serviceendpoint/crud_service_endpoint.go index 9313a9eec..3e1577df3 100644 --- a/azuredevops/crud/serviceendpoint/crud_service_endpoint.go +++ b/azuredevops/crud/serviceendpoint/crud_service_endpoint.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/config" "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/tfhelper" "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/validate" ) @@ -105,6 +106,31 @@ func GetScheme(d *schema.ResourceData) (string, error) { return scheme, nil } +// MakeProtectedSchema create protected schema +func MakeProtectedSchema(r *schema.Resource, keyName, envVarName, description string) { + r.Schema[keyName] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(envVarName, nil), + Description: description, + Sensitive: true, + DiffSuppressFunc: tfhelper.DiffFuncSuppressSecretChanged, + } + + secretHashKey, secretHashSchema := tfhelper.GenerateSecreteMemoSchema(keyName) + r.Schema[secretHashKey] = secretHashSchema +} + +// MakeUnprotectedSchema create unprotected schema +func MakeUnprotectedSchema(r *schema.Resource, keyName, envVarName, description string) { + r.Schema[keyName] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(envVarName, nil), + Description: description, + } +} + // Make the Azure DevOps API call to create the endpoint func createServiceEndpoint(clients *config.AggregatedClient, endpoint *serviceendpoint.ServiceEndpoint, project *string) (*serviceendpoint.ServiceEndpoint, error) { if *endpoint.Type == "github" && *endpoint.Authorization.Scheme == "InstallationToken" { diff --git a/azuredevops/provider.go b/azuredevops/provider.go index ae14fdddd..60aaa999a 100644 --- a/azuredevops/provider.go +++ b/azuredevops/provider.go @@ -14,6 +14,7 @@ func Provider() *schema.Provider { "azuredevops_variable_group": resourceVariableGroup(), "azuredevops_serviceendpoint_github": resourceServiceEndpointGitHub(), "azuredevops_serviceendpoint_dockerhub": resourceServiceEndpointDockerHub(), + "azuredevops_serviceendpoint_azurerm": resourceServiceEndpointAzureRM(), "azuredevops_azure_git_repository": resourceAzureGitRepository(), "azuredevops_user_entitlement": resourceUserEntitlement(), "azuredevops_group_membership": resourceGroupMembership(), diff --git a/azuredevops/provider_test.go b/azuredevops/provider_test.go index 0e2a9c659..e47d7a51a 100644 --- a/azuredevops/provider_test.go +++ b/azuredevops/provider_test.go @@ -20,6 +20,7 @@ func TestAzureDevOpsProvider_HasChildResources(t *testing.T) { "azuredevops_project", "azuredevops_serviceendpoint_github", "azuredevops_serviceendpoint_dockerhub", + "azuredevops_serviceendpoint_azurerm", "azuredevops_variable_group", "azuredevops_azure_git_repository", "azuredevops_user_entitlement", diff --git a/azuredevops/resource_project.go b/azuredevops/resource_project.go index 00e45c43d..9f397234f 100644 --- a/azuredevops/resource_project.go +++ b/azuredevops/resource_project.go @@ -141,7 +141,7 @@ func resourceProjectRead(d *schema.ResourceData, m interface{}) error { id := d.Id() name := d.Get("project_name").(string) - project, err := projectRead(clients, id, name) + project, err := ProjectRead(clients, id, name) if err != nil { return fmt.Errorf("Error looking up project with ID %s and Name %s", id, name) } @@ -153,10 +153,10 @@ func resourceProjectRead(d *schema.ResourceData, m interface{}) error { return nil } -// Lookup a project using the ID, or name if the ID is not set. Note, usage of the name in place +// ProjectRead Lookup a project using the ID, or name if the ID is not set. Note, usage of the name in place // of the ID is an explicitly stated supported behavior: // https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/get?view=azure-devops-rest-5.0 -func projectRead(clients *config.AggregatedClient, projectID string, projectName string) (*core.TeamProject, error) { +func ProjectRead(clients *config.AggregatedClient, projectID string, projectName string) (*core.TeamProject, error) { identifier := projectID if identifier == "" { identifier = projectName @@ -338,7 +338,7 @@ func ParseImportedProjectIDAndID(clients *config.AggregatedClient, id string) (s } // Get the project ID - currentProject, err := projectRead(clients, project, project) + currentProject, err := ProjectRead(clients, project, project) if err != nil { return "", 0, err } @@ -354,7 +354,7 @@ func ParseImportedProjectIDAndUUID(clients *config.AggregatedClient, id string) } // Get the project ID - currentProject, err := projectRead(clients, project, project) + currentProject, err := ProjectRead(clients, project, project) if err != nil { return "", "", err } diff --git a/azuredevops/resource_project_test.go b/azuredevops/resource_project_test.go index 2438bd4ef..7b68301d6 100644 --- a/azuredevops/resource_project_test.go +++ b/azuredevops/resource_project_test.go @@ -241,7 +241,7 @@ func TestAzureDevOpsProject_ProjectRead_UsesIdIfSet(t *testing.T) { }). Times(1) - projectRead(clients, id, name) + ProjectRead(clients, id, name) } // verifies that the project name is used for reads if the ID is not set @@ -267,7 +267,7 @@ func TestAzureDevOpsProject_ProjectRead_UsesNameIfIdNotSet(t *testing.T) { }). Times(1) - projectRead(clients, id, name) + ProjectRead(clients, id, name) } // creates an operation given a status @@ -340,7 +340,7 @@ func testAccCheckProjectResourceExists(expectedName string) resource.TestCheckFu clients := testAccProvider.Meta().(*config.AggregatedClient) id := resource.Primary.ID - project, err := projectRead(clients, id, "") + project, err := ProjectRead(clients, id, "") if err != nil { return fmt.Errorf("Project with ID=%s cannot be found!. Error=%v", id, err) @@ -369,7 +369,7 @@ func testAccProjectCheckDestroy(s *terraform.State) error { id := resource.Primary.ID // indicates the project still exists - this should fail the test - if _, err := projectRead(clients, id, ""); err == nil { + if _, err := ProjectRead(clients, id, ""); err == nil { return fmt.Errorf("project with ID %s should not exist", id) } } diff --git a/azuredevops/resource_serviceendpoint_azurerm.go b/azuredevops/resource_serviceendpoint_azurerm.go new file mode 100644 index 000000000..0e99b2bcc --- /dev/null +++ b/azuredevops/resource_serviceendpoint_azurerm.go @@ -0,0 +1,57 @@ +package azuredevops + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" + crud "github.com/microsoft/terraform-provider-azuredevops/azuredevops/crud/serviceendpoint" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/tfhelper" +) + +func resourceServiceEndpointAzureRM() *schema.Resource { + r := crud.GenBaseServiceEndpointResource(flattenServiceEndpointAzureRM, expandServiceEndpointAzureRM, parseImportedProjectIDAndServiceEndpointID) + crud.MakeUnprotectedSchema(r, "azurerm_spn_clientid", "ARM_CLIENT_ID", "The service principal id which should be used.") + crud.MakeProtectedSchema(r, "azurerm_spn_clientsecret", "ARM_CLIENT_SECRET", "The service principal secret which should be used.") + crud.MakeUnprotectedSchema(r, "azurerm_spn_tenantid", "ARM_TENANT_ID", "The service principal tenant id which should be used.") + crud.MakeUnprotectedSchema(r, "azurerm_subscription_id", "ARM_SUBSCRIPTION_ID", "The Azure subscription Id which should be used.") + crud.MakeUnprotectedSchema(r, "azurerm_subscription_name", "ARM_SUBSCRIPTION_NAME", "The Azure subscription name which should be used.") + crud.MakeUnprotectedSchema(r, "azurerm_scope", "ARM_SCOPE", "The Azure scope which should be used by the spn.") + return r +} + +// Convert internal Terraform data structure to an AzDO data structure +func expandServiceEndpointAzureRM(d *schema.ResourceData) (*serviceendpoint.ServiceEndpoint, *string) { + serviceEndpoint, projectID := crud.DoBaseExpansion(d) + serviceEndpoint.Authorization = &serviceendpoint.EndpointAuthorization{ + Parameters: &map[string]string{ + "authenticationType": "spnKey", + "scope": d.Get("azurerm_scope").(string), + "serviceprincipalid": d.Get("azurerm_spn_clientid").(string), + "serviceprincipalkey": d.Get("azurerm_spn_clientsecret").(string), + "tenantid": d.Get("azurerm_spn_tenantid").(string), + }, + Scheme: converter.String("ServicePrincipal"), + } + serviceEndpoint.Data = &map[string]string{ + "creationMode": "Manual", + "environment": "AzureCloud", + "scopeLevel": "Subscription", + "SubscriptionId": d.Get("azurerm_subscription_id").(string), + "SubscriptionName": d.Get("azurerm_subscription_name").(string), + } + serviceEndpoint.Type = converter.String("azurerm") + serviceEndpoint.Url = converter.String("https://management.azure.com/") + return serviceEndpoint, projectID +} + +// Convert AzDO data structure to internal Terraform data structure +func flattenServiceEndpointAzureRM(d *schema.ResourceData, serviceEndpoint *serviceendpoint.ServiceEndpoint, projectID *string) { + crud.DoBaseFlattening(d, serviceEndpoint, projectID) + d.Set("azurerm_scope", (*serviceEndpoint.Authorization.Parameters)["scope"]) + d.Set("azurerm_spn_clientid", (*serviceEndpoint.Authorization.Parameters)["serviceprincipalid"]) + tfhelper.HelpFlattenSecret(d, "azurerm_spn_clientsecret") + d.Set("azurerm_spn_tenantid", (*serviceEndpoint.Authorization.Parameters)["tenantid"]) + d.Set("azurerm_spn_clientsecret", (*serviceEndpoint.Authorization.Parameters)["serviceprincipalkey"]) + d.Set("azurerm_subscription_id", (*serviceEndpoint.Data)["SubscriptionId"]) + d.Set("azurerm_subscription_name", (*serviceEndpoint.Data)["SubscriptionName"]) +} diff --git a/azuredevops/resource_serviceendpoint_azurerm_test.go b/azuredevops/resource_serviceendpoint_azurerm_test.go new file mode 100644 index 000000000..94e4f7218 --- /dev/null +++ b/azuredevops/resource_serviceendpoint_azurerm_test.go @@ -0,0 +1,275 @@ +// +build all resource_serviceendpoint_azurerm + +package azuredevops + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/config" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/testhelper" + "github.com/stretchr/testify/require" + + "github.com/google/uuid" + + "github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint" +) + +var azurermTestServiceEndpointAzureRMID = uuid.New() +var azurermRandomServiceEndpointAzureRMProjectID = uuid.New().String() +var azurermTestServiceEndpointAzureRMProjectID = &azurermRandomServiceEndpointAzureRMProjectID + +var azurermTestServiceEndpointAzureRM = serviceendpoint.ServiceEndpoint{ + Authorization: &serviceendpoint.EndpointAuthorization{ + Parameters: &map[string]string{ + "authenticationType": "spnKey", + "scope": "/subscriptions/fa8e7d5e-84f9-4477-904f-852054f85586", //fake value + "serviceprincipalid": "e31eaaac-47da-4156-b433-9b0538c94b7e", //fake value + "serviceprincipalkey": "d96d8515-20b2-4413-8879-27c5d040cbc2", //fake value + "tenantid": "aba07645-051c-44b4-b806-c34d33f3dcd1", //fake value + }, + Scheme: converter.String("ServicePrincipal"), + }, + Data: &map[string]string{ + "creationMode": "Manual", + "environment": "AzureCloud", + "scopeLevel": "Subscription", + "SubscriptionId": "42125daf-72fd-417c-9ea7-080690625ad3", //fake value + "SubscriptionName": "SUBSCRIPTION_TEST", + }, + Id: &azurermTestServiceEndpointAzureRMID, + Name: converter.String("_AZURERM_UNIT_TEST_CONN_NAME"), + Description: converter.String("_AZURERM_UNIT_TEST_CONN_DESCRIPTION"), + Owner: converter.String("library"), // Supported values are "library", "agentcloud" + Type: converter.String("azurerm"), + Url: converter.String("https://management.azure.com/"), +} + +/** + * Begin unit tests + */ + +// verifies that the flatten/expand round trip yields the same service endpoint +func TestAzureDevOpsServiceEndpointAzureRM_ExpandFlatten_Roundtrip(t *testing.T) { + resourceData := schema.TestResourceDataRaw(t, resourceServiceEndpointAzureRM().Schema, nil) + flattenServiceEndpointAzureRM(resourceData, &azurermTestServiceEndpointAzureRM, azurermTestServiceEndpointAzureRMProjectID) + + serviceEndpointAfterRoundTrip, projectID := expandServiceEndpointAzureRM(resourceData) + + require.Equal(t, azurermTestServiceEndpointAzureRM, *serviceEndpointAfterRoundTrip) + require.Equal(t, azurermTestServiceEndpointAzureRMProjectID, projectID) +} + +// verifies that if an error is produced on create, the error is not swallowed +func TestAzureDevOpsServiceEndpointAzureRM_Create_DoesNotSwallowError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := resourceServiceEndpointAzureRM() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointAzureRM(resourceData, &azurermTestServiceEndpointAzureRM, azurermTestServiceEndpointAzureRMProjectID) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &config.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.CreateServiceEndpointArgs{Endpoint: &azurermTestServiceEndpointAzureRM, Project: azurermTestServiceEndpointAzureRMProjectID} + buildClient. + EXPECT(). + CreateServiceEndpoint(clients.Ctx, expectedArgs). + Return(nil, errors.New("CreateServiceEndpoint() Failed")). + Times(1) + + err := r.Create(resourceData, clients) + require.Contains(t, err.Error(), "CreateServiceEndpoint() Failed") +} + +// verifies that if an error is produced on a read, it is not swallowed +func TestAzureDevOpsServiceEndpointAzureRM_Read_DoesNotSwallowError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := resourceServiceEndpointAzureRM() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointAzureRM(resourceData, &azurermTestServiceEndpointAzureRM, azurermTestServiceEndpointAzureRMProjectID) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &config.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.GetServiceEndpointDetailsArgs{EndpointId: azurermTestServiceEndpointAzureRM.Id, Project: azurermTestServiceEndpointAzureRMProjectID} + buildClient. + EXPECT(). + GetServiceEndpointDetails(clients.Ctx, expectedArgs). + Return(nil, errors.New("GetServiceEndpoint() Failed")). + Times(1) + + err := r.Read(resourceData, clients) + require.Contains(t, err.Error(), "GetServiceEndpoint() Failed") +} + +// verifies that if an error is produced on a delete, it is not swallowed +func TestAzureDevOpsServiceEndpointAzureRM_Delete_DoesNotSwallowError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := resourceServiceEndpointAzureRM() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointAzureRM(resourceData, &azurermTestServiceEndpointAzureRM, azurermTestServiceEndpointAzureRMProjectID) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &config.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.DeleteServiceEndpointArgs{EndpointId: azurermTestServiceEndpointAzureRM.Id, Project: azurermTestServiceEndpointAzureRMProjectID} + buildClient. + EXPECT(). + DeleteServiceEndpoint(clients.Ctx, expectedArgs). + Return(errors.New("DeleteServiceEndpoint() Failed")). + Times(1) + + err := r.Delete(resourceData, clients) + require.Contains(t, err.Error(), "DeleteServiceEndpoint() Failed") +} + +// verifies that if an error is produced on an update, it is not swallowed +func TestAzureDevOpsServiceEndpointAzureRM_Update_DoesNotSwallowError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := resourceServiceEndpointAzureRM() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointAzureRM(resourceData, &azurermTestServiceEndpointAzureRM, azurermTestServiceEndpointAzureRMProjectID) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &config.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.UpdateServiceEndpointArgs{ + Endpoint: &azurermTestServiceEndpointAzureRM, + EndpointId: azurermTestServiceEndpointAzureRM.Id, + Project: azurermTestServiceEndpointAzureRMProjectID, + } + + buildClient. + EXPECT(). + UpdateServiceEndpoint(clients.Ctx, expectedArgs). + Return(nil, errors.New("UpdateServiceEndpoint() Failed")). + Times(1) + + err := r.Update(resourceData, clients) + require.Contains(t, err.Error(), "UpdateServiceEndpoint() Failed") +} + +/** + * Begin acceptance tests + */ + +// validates that an apply followed by another apply (i.e., resource update) will be reflected in AzDO and the +// underlying terraform state. +func TestAccAzureDevOpsServiceEndpointAzureRm_CreateAndUpdate(t *testing.T) { + projectName := testhelper.TestAccResourcePrefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + serviceEndpointNameFirst := testhelper.TestAccResourcePrefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + serviceEndpointNameSecond := testhelper.TestAccResourcePrefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + tfSvcEpNode := "azuredevops_serviceendpoint_azurerm.serviceendpointrm" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testhelper.TestAccPreCheck(t, nil) }, + Providers: testAccProviders, + CheckDestroy: testAccServiceEndpointAzureRMCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testhelper.TestAccServiceEndpointAzureRMResource(projectName, serviceEndpointNameFirst), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_clientid"), + resource.TestCheckResourceAttr(tfSvcEpNode, "azurerm_spn_clientsecret", ""), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_tenantid"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_clientsecret_hash"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameFirst), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_subscription_id"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_subscription_name"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_scope"), + testAccCheckServiceEndpointAzureRMResourceExists(serviceEndpointNameFirst), + ), + }, { + Config: testhelper.TestAccServiceEndpointAzureRMResource(projectName, serviceEndpointNameSecond), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_clientid"), + resource.TestCheckResourceAttr(tfSvcEpNode, "azurerm_spn_clientsecret", ""), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_tenantid"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_clientsecret_hash"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_subscription_id"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_subscription_name"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_scope"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameSecond), + testAccCheckServiceEndpointAzureRMResourceExists(serviceEndpointNameSecond), + ), + }, + }, + }) +} + +// Given the name of an AzDO service endpoint, this will return a function that will check whether +// or not the resource (1) exists in the state and (2) exist in AzDO and (3) has the correct name +func testAccCheckServiceEndpointAzureRMResourceExists(expectedName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + serviceEndpointDef, ok := s.RootModule().Resources["azuredevops_serviceendpoint_azurerm.serviceendpointrm"] + if !ok { + return fmt.Errorf("Did not find a service endpoint in the TF state") + } + + serviceEndpoint, err := getServiceEndpointAzureRMFromResource(serviceEndpointDef) + if err != nil { + return err + } + + if *serviceEndpoint.Name != expectedName { + return fmt.Errorf("Service Endpoint has Name=%s, but expected Name=%s", *serviceEndpoint.Name, expectedName) + } + + return nil + } +} + +// verifies that all service endpoints referenced in the state are destroyed. This will be invoked +// *after* terrafform destroys the resource but *before* the state is wiped clean. +func testAccServiceEndpointAzureRMCheckDestroy(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azuredevops_serviceendpoint_azurerm" { + continue + } + + // indicates the service endpoint still exists - this should fail the test + if _, err := getServiceEndpointAzureRMFromResource(resource); err == nil { + return fmt.Errorf("Unexpectedly found a service endpoint that should be deleted") + } + } + + return nil +} + +// given a resource from the state, return a service endpoint (and error) +func getServiceEndpointAzureRMFromResource(resource *terraform.ResourceState) (*serviceendpoint.ServiceEndpoint, error) { + serviceEndpointDefID, err := uuid.Parse(resource.Primary.ID) + if err != nil { + return nil, err + } + + projectID := resource.Primary.Attributes["project_id"] + clients := testAccProvider.Meta().(*config.AggregatedClient) + return clients.ServiceEndpointClient.GetServiceEndpointDetails(clients.Ctx, serviceendpoint.GetServiceEndpointDetailsArgs{ + Project: &projectID, + EndpointId: &serviceEndpointDefID, + }) +} + +func init() { + InitProvider() +} diff --git a/azuredevops/resource_serviceendpoint_dockerhub.go b/azuredevops/resource_serviceendpoint_dockerhub.go index d3e99f159..4adf23bc8 100644 --- a/azuredevops/resource_serviceendpoint_dockerhub.go +++ b/azuredevops/resource_serviceendpoint_dockerhub.go @@ -80,7 +80,7 @@ func parseImportedProjectIDAndServiceEndpointID(clients *config.AggregatedClient } // Get the project ID - currentProject, err := projectRead(clients, project, project) + currentProject, err := ProjectRead(clients, project, project) if err != nil { return "", "", err } diff --git a/azuredevops/resource_variable_group.go b/azuredevops/resource_variable_group.go index 60a846602..5e693af8d 100644 --- a/azuredevops/resource_variable_group.go +++ b/azuredevops/resource_variable_group.go @@ -348,3 +348,19 @@ func flattenAllowAccess(d *schema.ResourceData, definitionResource *[]build.Defi } d.Set("allow_access", allowAccess) } + +// ParseImportedProjectIDAndVariableGroupID : Parse the Id (projectId/variableGroupId) or (projectName/variableGroupId) +func ParseImportedProjectIDAndVariableGroupID(clients *config.AggregatedClient, id string) (string, int, error) { + project, resourceID, err := tfhelper.ParseImportedID(id) + if err != nil { + return "", 0, err + } + + // Get the project ID + currentProject, err := ProjectRead(clients, project, project) + if err != nil { + return "", 0, err + } + + return currentProject.Id.String(), resourceID, nil +} diff --git a/azuredevops/utils/testhelper/hcl.go b/azuredevops/utils/testhelper/hcl.go index 1b81ce967..f849b6663 100644 --- a/azuredevops/utils/testhelper/hcl.go +++ b/azuredevops/utils/testhelper/hcl.go @@ -82,6 +82,25 @@ resource "azuredevops_serviceendpoint_dockerhub" "serviceendpoint" { return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) } +// TestAccServiceEndpointAzureRMResource HCL describing an AzDO service endpoint +func TestAccServiceEndpointAzureRMResource(projectName string, serviceEndpointName string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_azurerm" "serviceendpointrm" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + azurerm_spn_clientid ="e318e66b-ec4b-4dff-9124-41129b9d7150" + azurerm_spn_tenantid = "9c59cbe5-2ca1-4516-b303-8968a070edd2" + azurerm_subscription_id = "3b0fee91-c36d-4d70-b1e9-fc4b9d608c3d" + azurerm_subscription_name = "Microsoft Azure DEMO" + azurerm_scope = "/subscriptions/3b0fee91-c36d-4d70-b1e9-fc4b9d608c3d" + azurerm_spn_clientsecret ="d9d210dd-f9f0-4176-afb8-a4df60e1ae72" + +}`, serviceEndpointName) + + projectResource := TestAccProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + // TestAccVariableGroupResource HCL describing an AzDO variable group func TestAccVariableGroupResource(projectName string, variableGroupName string, allowAccess bool) string { variableGroupResource := fmt.Sprintf(` diff --git a/azuredevops/utils/tfhelper/tfhelper.go b/azuredevops/utils/tfhelper/tfhelper.go index b97e54481..2cefdceb6 100644 --- a/azuredevops/utils/tfhelper/tfhelper.go +++ b/azuredevops/utils/tfhelper/tfhelper.go @@ -114,6 +114,18 @@ func ParseImportedID(id string) (string, int, error) { return project, resourceID, nil } +// ParseImportedName parse the imported Id (Name) from the terraform import +func ParseImportedName(id string) (string, string, error) { + parts := strings.SplitN(id, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected projectid/resourceName", id) + } + project := parts[0] + resourceID := parts[1] + + return project, resourceID, nil +} + // ParseImportedUUID parse the imported uuid from the terraform import func ParseImportedUUID(id string) (string, string, error) { parts := strings.SplitN(id, "/", 2) diff --git a/examples/azdo-based-cicd/main.tf b/examples/azdo-based-cicd/main.tf index 6e1db5a85..dd25b388f 100644 --- a/examples/azdo-based-cicd/main.tf +++ b/examples/azdo-based-cicd/main.tf @@ -83,6 +83,19 @@ resource "azuredevops_azure_git_repository" "repository" { init_type = "Clean" } } + +// Configuration of AzureRm service end point +resource "azuredevops_serviceendpoint_azurerm" "endpoint1" { + project_id = azuredevops_project.project.id + service_endpoint_name = "TestServiceAzureRM" + azurerm_spn_clientid = "ee7f75a0-8553-4e6a-xxxx-xxxxxxxx" + azurerm_spn_clientsecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + azurerm_spn_tenantid = "2e3a33f9-66b1-4xxx-xxxx-xxxxxxxxx" + azurerm_subscription_id = "8a7aace5-xxxx-xxxx-xxxx-xxxxxxxxxx" + azurerm_subscription_name = "Microsoft Azure DEMO" + azurerm_scope = "/subscriptions/1da42ac9-xxxx-xxxxx-xxxx-xxxxxxxxxxx" +} + # # https://github.com/microsoft/terraform-provider-azuredevops/issues/83 # resource "azuredevops_policy_build" "p1" { diff --git a/go.mod b/go.mod index caf2b27d4..a00479a86 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/golang/mock v1.3.1 github.com/google/uuid v1.1.1 github.com/hashicorp/go-uuid v1.0.1 - github.com/hashicorp/terraform v0.12.17 + github.com/hashicorp/terraform v0.12.18 github.com/hashicorp/terraform-plugin-sdk v1.1.1 - github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191018194956-273e55a7119a + github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191125191507-ad702f5ae0cd github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect diff --git a/go.sum b/go.sum index 26d23cd88..aabb6d3e8 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,10 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE= github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/hashicorp/terraform v0.12.16 h1:YrIOUXUmOZws9cDzbq6F908tRo9tKDh93zCRlVm0PA4= +github.com/hashicorp/terraform v0.12.16/go.mod h1:d9WgReBJXp2flEs5x4VyG4SznxezqJklqtdWnLEqCqI= +github.com/hashicorp/terraform v0.12.18 h1:U9gd/12wfT0Q7JYM43Hob6rcirICKCnxSDY+sJlYh6A= +github.com/hashicorp/terraform v0.12.18/go.mod h1:wA1HxKwR2a21mNFaKyv1lQ+dAwtQKCKFfUAuTqPeP2U= github.com/hashicorp/terraform v0.12.17 h1:U4gOJWeG1/ICNS4CHSTjFsM7hxOM1t+2GDTq2TDiCLg= github.com/hashicorp/terraform v0.12.17/go.mod h1:LQR1l9+qbkA0ZKujo+7dk3tSKhYmG5LQjogPW6DZblI= github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4 h1:fTkL0YwjohGyN7AqsDhz6bwcGBpT+xBqi3Qhpw58Juw= @@ -241,8 +245,11 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microsoft/azure-devops-go-api v0.0.0-20191125191507-ad702f5ae0cd h1:02GtAx2PVjhd92CoTIyYB/gFbWGxxa6jEZE4E4ptjqA= github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191018194956-273e55a7119a h1:+t1mM7zrb4HrBd0IdFrtFSvLiHfNNxQIEEeU6U9K81Y= github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191018194956-273e55a7119a/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= +github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191125191507-ad702f5ae0cd h1:+SP1ImizQl7sJczJlCgDos9xqt3rLqOZIqa3bXqd1N4= +github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191125191507-ad702f5ae0cd/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= diff --git a/website/docs/r/serviceendpoint_azurerm.html.markdown b/website/docs/r/serviceendpoint_azurerm.html.markdown new file mode 100644 index 000000000..810106507 --- /dev/null +++ b/website/docs/r/serviceendpoint_azurerm.html.markdown @@ -0,0 +1,53 @@ +# azuredevops_serviceendpoint_azurerm +Manages a AzureRM service endpoint within Azure DevOps. + +## Requirements +Before to create a service end point in Azure DevOps, you need to create a Service Principal in your Azure subscription. + +For detailled steps to create a service principal with Azure cli see the [documentation](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) + +## Example Usage + +```hcl +resource "azuredevops_project" "project" { + project_name = "Sample Project" + visibility = "private" + version_control = "Git" + work_item_template = "Agile" +} + +resource "azuredevops_serviceendpoint_azurerm" "endpointazure" { + project_id = azuredevops_project.project.id + service_endpoint_name = "TestServiceRM" + azurerm_spn_clientid = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx" + azurerm_spn_clientsecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + azurerm_spn_tenantid = "xxxxxxx-xxxx-xxx-xxxxx-xxxxxxxx" + azurerm_subscription_id = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx" + azurerm_subscription_name = "Microsoft Azure DEMO" + azurerm_scope = "/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `project_id` - (Required) The project ID or project name. +* `service_endpoint_name` - (Required) The Service Endpoint name. +* `azurerm_spn_clientid` - (Required) The service principal application Id +* `azurerm_spn_clientsecret` - (Required) The service principal secret. +* `azurerm_spn_tenantid` - (Required) The tenant id if the service principal. +* `azurerm_subscription_id` - (Required) The subscription Id of the Azure targets. +* `azurerm_subscription_name` - (Required) The subscription Name of the targets. +* `azurerm_scope` - (Required) The Azure scope of the end point (ID of the subscription or resource group). + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the service endpoint. +* `project_id` - The project ID or project name. +* `service_endpoint_name` - The Service Endpoint name. + +## Relevant Links +* [Azure DevOps Service REST API 5.1 - Service End points](https://docs.microsoft.com/en-us/rest/api/azure/devops/serviceendpoint/endpoints?view=azure-devops-rest-5.1)