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

adding validation feature to azuredevops_serviceendpoint_azurerm reso… #865

Merged
merged 8 commits into from
Nov 14, 2023
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ lint:
test: fmtcheck
go test -tags "all" -i $(UNITTEST) || exit 1
echo $(UNITTEST) | \
xargs -t -n4 go test -tags "all" $(TESTARGS) -timeout=60s -parallel=4
xargs -t -n4 go test -tags "all" $(TESTARGS) -timeout=120s -parallel=4

testacc: fmtcheck
@echo "==> Sourcing .env file if available"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path"
"regexp"
"strconv"
"testing"

"github.com/google/uuid"
Expand Down Expand Up @@ -67,6 +68,65 @@ func TestAccServiceEndpointAzureRm_CreateAndUpdate(t *testing.T) {
})
}

func TestAccServiceEndpointAzureRm_CreateAndUpdate_WithValidate(t *testing.T) {
projectName := testutils.GenerateResourceName()
serviceEndpointNameFirst := testutils.GenerateResourceName()
serviceprincipalidFirst := uuid.New().String()
serviceprincipalkeyFirst := uuid.New().String()
serviceEndpointAuthenticationScheme := "ServicePrincipal"
validateFirst := false
validateSecond := true

resourceType := "azuredevops_serviceendpoint_azurerm"
tfSvcEpNode := resourceType + ".serviceendpointrm"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType),
Steps: []resource.TestStep{
{
Config: testutils.HclServiceEndpointAzureRMResourceWithValidate(projectName, serviceEndpointNameFirst, serviceprincipalidFirst, serviceprincipalkeyFirst, serviceEndpointAuthenticationScheme, validateFirst),
Check: resource.ComposeTestCheckFunc(
testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointNameFirst),
resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"),
resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_spn_tenantid"),
resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameFirst),
resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_subscription_id"),
resource.TestCheckResourceAttrSet(tfSvcEpNode, "azurerm_subscription_name"),
resource.TestCheckResourceAttr(tfSvcEpNode, "credentials.0.serviceprincipalid", serviceprincipalidFirst),
resource.TestCheckResourceAttr(tfSvcEpNode, "credentials.0.serviceprincipalkey", serviceprincipalkeyFirst),
resource.TestCheckResourceAttr(tfSvcEpNode, "features.0.validate", strconv.FormatBool(validateFirst)),
),
}, {
Config: testutils.HclServiceEndpointAzureRMResourceWithValidate(projectName, serviceEndpointNameFirst, serviceprincipalidFirst, serviceprincipalkeyFirst, serviceEndpointAuthenticationScheme, validateSecond),
ExpectError: regexp.MustCompile("Failed to obtain the Json Web Token"),
},
},
})
}

func TestAccServiceEndpointAzureRm_Create_WithValidate(t *testing.T) {
projectName := testutils.GenerateResourceName()
serviceEndpointName := testutils.GenerateResourceName()
serviceprincipalid := uuid.New().String()
serviceprincipalkey := uuid.New().String()
serviceEndpointAuthenticationScheme := "ServicePrincipal"
validate := true

resourceType := "azuredevops_serviceendpoint_azurerm"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType),
Steps: []resource.TestStep{
{
Config: testutils.HclServiceEndpointAzureRMResourceWithValidate(projectName, serviceEndpointName, serviceprincipalid, serviceprincipalkey, serviceEndpointAuthenticationScheme, validate),
ExpectError: regexp.MustCompile("Failed to obtain the Json Web Token"),
},
},
})
}

func TestAccServiceEndpointAzureRm_MgmtGrpCreateAndUpdate(t *testing.T) {
projectName := testutils.GenerateResourceName()
serviceEndpointName := testutils.GenerateResourceName()
Expand Down
23 changes: 23 additions & 0 deletions azuredevops/internal/acceptancetests/testutils/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,29 @@ resource "azuredevops_serviceendpoint_azurerm" "serviceendpointrm" {
return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource)
}

func HclServiceEndpointAzureRMResourceWithValidate(projectName string, serviceEndpointName string, serviceprincipalid string, serviceprincipalkey string, serviceEndpointAuthenticationScheme string, validate bool) string {
serviceEndpointResource := fmt.Sprintf(`
resource "azuredevops_serviceendpoint_azurerm" "serviceendpointrm" {
project_id = azuredevops_project.project.id
service_endpoint_name = "%s"
credentials {
serviceprincipalid = "%s"
serviceprincipalkey = "%s"
}
azurerm_spn_tenantid = "9c59cbe5-2ca1-4516-b303-8968a070edd2"
azurerm_subscription_id = "3b0fee91-c36d-4d70-b1e9-fc4b9d608c3d"
azurerm_subscription_name = "Microsoft Azure DEMO"
service_endpoint_authentication_scheme = "%s"
features {
validate = %v
}
}
`, serviceEndpointName, serviceprincipalid, serviceprincipalkey, serviceEndpointAuthenticationScheme, validate)

projectResource := HclProjectResource(projectName)
return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource)
}

// HclServiceEndpointAzureRMResource HCL describing an AzDO service endpoint
func HclServiceEndpointAzureRMNoKeyResource(projectName string, serviceEndpointName string, serviceprincipalid string, serviceEndpointAuthenticationScheme string) string {
serviceEndpointResource := fmt.Sprintf(`
Expand Down
36 changes: 36 additions & 0 deletions azuredevops/internal/service/serviceendpoint/commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package serviceendpoint
import (
"fmt"
"log"
"strings"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -138,6 +139,41 @@ func deleteServiceEndpoint(clients *client.AggregatedClient, projectID *uuid.UUI
return nil
}

func validateServiceEndpoint(clients *client.AggregatedClient, endpoint *serviceendpoint.ServiceEndpoint, serviceEndpointID *string, retryTimeout time.Duration) error {
reqArgs := serviceendpoint.ExecuteServiceEndpointRequestArgs{
ServiceEndpointRequest: &serviceendpoint.ServiceEndpointRequest{
DataSourceDetails: &serviceendpoint.DataSourceDetails{
DataSourceName: converter.String("TestConnection"),
},
ResultTransformationDetails: &serviceendpoint.ResultTransformationDetails{},
ServiceEndpointDetails: &serviceendpoint.ServiceEndpointDetails{
Data: endpoint.Data,
Authorization: endpoint.Authorization,
Url: endpoint.Url,
Type: endpoint.Type,
},
},
Project: converter.String((*endpoint.ServiceEndpointProjectReferences)[0].ProjectReference.Id.String()),
EndpointId: serviceEndpointID,
}

log.Printf(fmt.Sprintf(":: %s :: Initiating validation", *endpoint.Name))
err := resource.RetryContext(clients.Ctx, retryTimeout, func() *resource.RetryError {
reqResult, err := clients.ServiceEndpointClient.ExecuteServiceEndpointRequest(clients.Ctx, reqArgs)
if err != nil {
log.Printf(fmt.Sprintf(":: %s :: error during endpoint validation request", *endpoint.Name))
return resource.NonRetryableError(err)
}
if !strings.EqualFold(*reqResult.StatusCode, "ok") {
log.Printf(fmt.Sprintf(":: %s :: validation failed with StatusCode '%s', retrying...", *endpoint.Name, *reqResult.StatusCode))
return resource.RetryableError(fmt.Errorf("Error validating connection: (type: %s, name: %s, code: %s, message: %s)", *endpoint.Type, *endpoint.Name, *reqResult.StatusCode, *reqResult.ErrorMessage))
}
log.Printf(fmt.Sprintf(":: %s :: successfully validated connection", *endpoint.Name))
return nil
})
return err
}

func serviceEndpointGetArgs(d *schema.ResourceData) (*serviceendpoint.GetServiceEndpointDetailsArgs, error) {
var serviceEndpointID *uuid.UUID
parsedServiceEndpointID, err := uuid.Parse(d.Id())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/tfhelper"
)

const endpointValidationTimeoutSeconds = 60 * time.Second

// ResourceServiceEndpointAzureRM schema and implementation for AzureRM service endpoint resource
func ResourceServiceEndpointAzureRM() *schema.Resource {
r := &schema.Resource{
Expand Down Expand Up @@ -151,12 +153,28 @@ func ResourceServiceEndpointAzureRM() *schema.Resource {
Version: 1,
},
}

r.Schema["features"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"validate": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Whether or not to validate connection with azure after create or update operations",
},
},
},
}
return r
}

func resourceServiceEndpointAzureRMCreate(d *schema.ResourceData, m interface{}) error {
clients := m.(*client.AggregatedClient)
serviceEndpoint, _, err := expandServiceEndpointAzureRM(d)
serviceEndpoint, projectID, err := expandServiceEndpointAzureRM(d)
if err != nil {
return fmt.Errorf(errMsgTfConfigRead, err)
}
Expand All @@ -166,6 +184,22 @@ func resourceServiceEndpointAzureRMCreate(d *schema.ResourceData, m interface{})
return err
}

if shouldValidate(endpointFeatures(d)) {
if err := validateServiceEndpoint(clients, serviceEndpoint, converter.String(serviceEndPoint.Id.String()), endpointValidationTimeoutSeconds); err != nil {
if delErr := clients.ServiceEndpointClient.DeleteServiceEndpoint(
clients.Ctx,
serviceendpoint.DeleteServiceEndpointArgs{
ProjectIds: &[]string{
projectID.String(),
},
EndpointId: serviceEndPoint.Id,
}); delErr != nil {
return fmt.Errorf(" Delete service endpoint error %v", delErr)
}
return err
}
}

d.SetId(serviceEndPoint.Id.String())
return resourceServiceEndpointAzureRMRead(d, m)
}
Expand All @@ -191,6 +225,8 @@ func resourceServiceEndpointAzureRMRead(d *schema.ResourceData, m interface{}) e
return nil
}

d.Set("features", d.Get("features"))

flattenServiceEndpointAzureRM(d, serviceEndpoint, (*serviceEndpoint.ServiceEndpointProjectReferences)[0].ProjectReference.Id)
return nil
}
Expand All @@ -202,6 +238,11 @@ func resourceServiceEndpointAzureRMUpdate(d *schema.ResourceData, m interface{})
return fmt.Errorf(errMsgTfConfigRead, err)
}

if shouldValidate(endpointFeatures(d)) {
if err := validateServiceEndpoint(clients, serviceEndpoint, converter.String(serviceEndpoint.Id.String()), endpointValidationTimeoutSeconds); err != nil {
return err
}
}
updatedServiceEndpoint, err := updateServiceEndpoint(clients, serviceEndpoint)

if err != nil {
Expand Down Expand Up @@ -460,6 +501,22 @@ func validateScopeLevel(scopeMap map[string][]string) error {
return nil
}

func endpointFeatures(d *schema.ResourceData) map[string]interface{} {
features := d.Get("features").([]interface{})
if features != nil && len(features) != 0 {
return features[0].(map[string]interface{})
}
return nil
}

func shouldValidate(features map[string]interface{}) bool {
validate, ok := features["validate"].(bool)
if !ok {
return false
}
return validate
}

type AzureRmEndpointAuthenticationScheme string

const (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,60 @@ func TestServiceEndpointAzureRM_Create_DoesNotSwallowError(t *testing.T) {
}
}

func TestServiceEndpointAzureRM_CreateWithValidate_DoesNotSwallowError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

r := ResourceServiceEndpointAzureRM()
for _, resource := range azurermTestServiceEndpointsAzureRM {
resourceData := getResourceData(t, resource)
flattenServiceEndpointAzureRM(resourceData, &resource, azurermTestServiceEndpointAzureRMProjectID)

features := initializeFeaturesWithValidate(true)
resourceData.Set("features", features)

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

createArgs := serviceendpoint.CreateServiceEndpointArgs{Endpoint: &resource}

buildClient.
EXPECT().
CreateServiceEndpoint(clients.Ctx, createArgs).
Return(&resource, nil).
Times(1)

returnedServiceEndpoint := resource
returnedServiceEndpoint.IsReady = converter.Bool(true)
buildClient.
EXPECT().
GetServiceEndpointDetails(clients.Ctx, serviceendpoint.GetServiceEndpointDetailsArgs{
Project: converter.String(azurermRandomServiceEndpointAzureRMProjectID.String()),
EndpointId: resource.Id,
},
).
Return(&returnedServiceEndpoint, nil).
Times(1)

reqArgs := genExecuteServiceEndpointArgs(&resource)
buildClient.
EXPECT().
ExecuteServiceEndpointRequest(clients.Ctx, *reqArgs).
Return(nil, errors.New("ExecuteServiceEndpointRequest() Failed")).
Times(1)

buildClient.
EXPECT().
DeleteServiceEndpoint(clients.Ctx, serviceendpoint.DeleteServiceEndpointArgs{
ProjectIds: &[]string{azurermTestServiceEndpointAzureRMProjectID.String()}, EndpointId: resource.Id}).
Return(nil).
Times(1)

err := r.Create(resourceData, clients)
require.Contains(t, err.Error(), "ExecuteServiceEndpointRequest() Failed")
}
}

// verifies that if an error is produced on a read, it is not swallowed
func TestServiceEndpointAzureRM_Read_DoesNotSwallowError(t *testing.T) {
ctrl := gomock.NewController(t)
Expand Down Expand Up @@ -381,6 +435,33 @@ func TestServiceEndpointAzureRM_Update_DoesNotSwallowError(t *testing.T) {
}
}

func TestServiceEndpointAzureRM_UpdateWithValidate_DoesNotSwallowError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

r := ResourceServiceEndpointAzureRM()
for _, resource := range azurermTestServiceEndpointsAzureRM {
resourceData := getResourceData(t, resource)
flattenServiceEndpointAzureRM(resourceData, &resource, azurermTestServiceEndpointAzureRMProjectID)

features := initializeFeaturesWithValidate(true)
resourceData.Set("features", features)

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

reqArgs := genExecuteServiceEndpointArgs(&resource)
buildClient.
EXPECT().
ExecuteServiceEndpointRequest(clients.Ctx, *reqArgs).
Return(nil, errors.New("ExecuteServiceEndpointRequest() Failed")).
Times(1)

err := r.Update(resourceData, clients)
require.Contains(t, err.Error(), "ExecuteServiceEndpointRequest() Failed")
}
}

// This is a little different than most. The steps done, along with the motivation behind each, are as follows:
// (1) The service endpoint is configured. The `serviceprincipalkey` is set to `""`, which matches
// the Azure DevOps API behavior. The service will intentionally hide the value of
Expand Down Expand Up @@ -417,3 +498,30 @@ func getResourceData(t *testing.T, resource serviceendpoint.ServiceEndpoint) *sc
}
return resourceData
}

func genExecuteServiceEndpointArgs(endpoint *serviceendpoint.ServiceEndpoint) *serviceendpoint.ExecuteServiceEndpointRequestArgs {
return &serviceendpoint.ExecuteServiceEndpointRequestArgs{
ServiceEndpointRequest: &serviceendpoint.ServiceEndpointRequest{
DataSourceDetails: &serviceendpoint.DataSourceDetails{
DataSourceName: converter.String("TestConnection"),
},
ResultTransformationDetails: &serviceendpoint.ResultTransformationDetails{},
ServiceEndpointDetails: &serviceendpoint.ServiceEndpointDetails{
Data: endpoint.Data,
Authorization: endpoint.Authorization,
Url: endpoint.Url,
Type: endpoint.Type,
},
},
Project: converter.String((*endpoint.ServiceEndpointProjectReferences)[0].ProjectReference.Id.String()),
EndpointId: converter.String(endpoint.Id.String()),
}
}

func initializeFeaturesWithValidate(validate bool) []map[string]interface{} {
var features []map[string]interface{}
feature := make(map[string]interface{})
feature["validate"] = validate
features = append(features, feature)
return features
}
Loading