diff --git a/azuredevops/internal/acceptancetests/resource_serviceendpoint_argocd_test.go b/azuredevops/internal/acceptancetests/resource_serviceendpoint_argocd_test.go new file mode 100644 index 000000000..61180c0fd --- /dev/null +++ b/azuredevops/internal/acceptancetests/resource_serviceendpoint_argocd_test.go @@ -0,0 +1,368 @@ +//go:build (all || resource_serviceendpoint_argocd) && !exclude_serviceendpoints +// +build all resource_serviceendpoint_argocd +// +build !exclude_serviceendpoints + +package acceptancetests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils" +) + +func TestAccServiceEndpointArgoCD_basic(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointName := testutils.GenerateResourceName() + + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceBasic(projectName, serviceEndpointName, t.Name()), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointName), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "url"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointName), + ), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_basic_usernamepassword(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointName := testutils.GenerateResourceName() + + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceBasicUsernamePassword(projectName, serviceEndpointName, t.Name()), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointName), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "authentication_basic.#", "1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointName), + ), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_complete_token(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointName := testutils.GenerateResourceName() + description := t.Name() + + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceComplete(projectName, serviceEndpointName, description), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointName), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "authentication_token.#", "1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "url", "https://url.com/1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointName), + resource.TestCheckResourceAttr(tfSvcEpNode, "description", description), + ), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_complete_usernamepassword(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointName := testutils.GenerateResourceName() + description := t.Name() + + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceCompleteUsernamePassword(projectName, serviceEndpointName, description), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointName), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "authentication_basic.#", "1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "url", "https://url.com/1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointName), + resource.TestCheckResourceAttr(tfSvcEpNode, "description", description), + ), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_update(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointNameFirst := testutils.GenerateResourceName() + + description := t.Name() + serviceEndpointNameSecond := testutils.GenerateResourceName() + + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceBasic(projectName, serviceEndpointNameFirst, t.Name()), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointNameFirst), resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameFirst), + ), + }, + { + Config: hclSvcEndpointArgoCDResourceUpdate(projectName, serviceEndpointNameSecond, description), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointNameSecond), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "authentication_token.#", "1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "url", "https://url.com/2"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameSecond), + resource.TestCheckResourceAttr(tfSvcEpNode, "description", description), + ), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_update_usernamepassword(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointNameFirst := testutils.GenerateResourceName() + + description := t.Name() + serviceEndpointNameSecond := testutils.GenerateResourceName() + + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceBasicUsernamePassword(projectName, serviceEndpointNameFirst, t.Name()), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointNameFirst), resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameFirst), + ), + }, + { + Config: hclSvcEndpointArgoCDResourceUpdateUsernamePassword(projectName, serviceEndpointNameSecond, description), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointNameSecond), + resource.TestCheckResourceAttrSet(tfSvcEpNode, "project_id"), + resource.TestCheckResourceAttr(tfSvcEpNode, "authentication_basic.#", "1"), + resource.TestCheckResourceAttr(tfSvcEpNode, "url", "https://url.com/2"), + resource.TestCheckResourceAttr(tfSvcEpNode, "service_endpoint_name", serviceEndpointNameSecond), + resource.TestCheckResourceAttr(tfSvcEpNode, "description", description), + ), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_RequiresImportErrorStep(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointName := testutils.GenerateResourceName() + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceBasic(projectName, serviceEndpointName, t.Name()), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointName), + ), + }, + { + Config: hclSvcEndpointArgoCDResourceRequiresImport(projectName, serviceEndpointName, t.Name()), + ExpectError: testutils.RequiresImportError(serviceEndpointName), + }, + }, + }) +} + +func TestAccServiceEndpointArgoCD_RequiresImportErrorStepUsernamePassword(t *testing.T) { + projectName := testutils.GenerateResourceName() + serviceEndpointName := testutils.GenerateResourceName() + resourceType := "azuredevops_serviceendpoint_argocd" + tfSvcEpNode := resourceType + ".test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testutils.PreCheck(t, nil) }, + Providers: testutils.GetProviders(), + CheckDestroy: testutils.CheckServiceEndpointDestroyed(resourceType), + Steps: []resource.TestStep{ + { + Config: hclSvcEndpointArgoCDResourceBasicUsernamePassword(projectName, serviceEndpointName, t.Name()), + Check: resource.ComposeTestCheckFunc( + testutils.CheckServiceEndpointExistsWithName(tfSvcEpNode, serviceEndpointName), + ), + }, + { + Config: hclSvcEndpointArgoCDResourceRequiresImport(projectName, serviceEndpointName, t.Name()), + ExpectError: testutils.RequiresImportError(serviceEndpointName), + }, + }, + }) +} + +func hclSvcEndpointArgoCDResourceBasic(projectName string, serviceEndpointName string, description string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_argocd" "test" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + authentication_token { + token = "redacted" + } + url = "http://url.com/1" + description = "%s" +}`, serviceEndpointName, description) + + projectResource := testutils.HclProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + +func hclSvcEndpointArgoCDResourceBasicUsernamePassword(projectName string, serviceEndpointName string, description string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_argocd" "test" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + authentication_basic { + username = "u" + password = "redacted" + } + url = "http://url.com/1" + description = "%s" +}`, serviceEndpointName, description) + + projectResource := testutils.HclProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + +func hclSvcEndpointArgoCDResourceCompleteUsernamePassword(projectName string, serviceEndpointName string, description string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_argocd" "test" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + description = "%s" + authentication_basic { + username = "u" + password = "redacted" + } + url = "https://url.com/1" +}`, serviceEndpointName, description) + + projectResource := testutils.HclProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + +func hclSvcEndpointArgoCDResourceComplete(projectName string, serviceEndpointName string, description string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_argocd" "test" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + description = "%s" + authentication_token { + token = "redacted" + } + url = "https://url.com/1" +}`, serviceEndpointName, description) + + projectResource := testutils.HclProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + +func hclSvcEndpointArgoCDResourceUpdate(projectName string, serviceEndpointName string, description string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_argocd" "test" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + description = "%s" + authentication_token { + token = "redacted2" + } + url = "https://url.com/2" +}`, serviceEndpointName, description) + + projectResource := testutils.HclProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + +func hclSvcEndpointArgoCDResourceUpdateUsernamePassword(projectName string, serviceEndpointName string, description string) string { + serviceEndpointResource := fmt.Sprintf(` +resource "azuredevops_serviceendpoint_argocd" "test" { + project_id = azuredevops_project.project.id + service_endpoint_name = "%s" + description = "%s" + authentication_basic { + username = "u2" + password = "redacted2" + } + url = "https://url.com/2" +}`, serviceEndpointName, description) + + projectResource := testutils.HclProjectResource(projectName) + return fmt.Sprintf("%s\n%s", projectResource, serviceEndpointResource) +} + +func hclSvcEndpointArgoCDResourceRequiresImport(projectName string, serviceEndpointName string, description string) string { + template := hclSvcEndpointArgoCDResourceBasic(projectName, serviceEndpointName, description) + return fmt.Sprintf(` +%s +resource "azuredevops_serviceendpoint_argocd" "import" { + project_id = azuredevops_serviceendpoint_argocd.test.project_id + service_endpoint_name = azuredevops_serviceendpoint_argocd.test.service_endpoint_name + description = azuredevops_serviceendpoint_argocd.test.description + url = azuredevops_serviceendpoint_argocd.test.url + authentication_token { + token = "redacted" + } +} +`, template) +} +func hclSvcEndpointArgoCDResourceRequiresImportUsernamePassword(projectName string, serviceEndpointName string, description string) string { + template := hclSvcEndpointArgoCDResourceBasicUsernamePassword(projectName, serviceEndpointName, description) + return fmt.Sprintf(` +%s +resource "azuredevops_serviceendpoint_argocd" "import" { + project_id = azuredevops_serviceendpoint_argocd.test.project_id + service_endpoint_name = azuredevops_serviceendpoint_argocd.test.service_endpoint_name + description = azuredevops_serviceendpoint_argocd.test.description + url = azuredevops_serviceendpoint_argocd.test.url + authentication_basic { + username = "u" + password = "redacted" + } +} +`, template) +} diff --git a/azuredevops/internal/service/serviceendpoint/resource_serviceendpoint_argocd.go b/azuredevops/internal/service/serviceendpoint/resource_serviceendpoint_argocd.go new file mode 100644 index 000000000..2f7654d3e --- /dev/null +++ b/azuredevops/internal/service/serviceendpoint/resource_serviceendpoint_argocd.go @@ -0,0 +1,158 @@ +package serviceendpoint + +import ( + "fmt" + "strings" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/microsoft/azure-devops-go-api/azuredevops/v6/serviceendpoint" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/tfhelper" +) + +// ResourceServiceEndpointArgoCD schema and implementation for ArgoCD service endpoint resource +func ResourceServiceEndpointArgoCD() *schema.Resource { + r := genBaseServiceEndpointResource(flattenServiceEndpointArgoCD, expandServiceEndpointArgoCD) + + r.Schema["url"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: func(i interface{}, key string) (_ []string, errors []error) { + url, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", key)) + return + } + if strings.HasSuffix(url, "/") { + errors = append(errors, fmt.Errorf("%q should not end with slash, got %q.", key, url)) + return + } + return validation.IsURLWithHTTPorHTTPS(url, key) + }, + Description: "Url for the ArgoCD Server", + } + + patHashKey, patHashSchema := tfhelper.GenerateSecreteMemoSchema("token") + at := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "token": { + Description: "The ArgoCD access token.", + Type: schema.TypeString, + Required: true, + Sensitive: true, + DiffSuppressFunc: tfhelper.DiffFuncSuppressSecretChanged, + }, + patHashKey: patHashSchema, + }, + } + + patHashKeyU, patHashSchemaU := tfhelper.GenerateSecreteMemoSchema("username") + patHashKeyP, patHashSchemaP := tfhelper.GenerateSecreteMemoSchema("password") + aup := &schema.Resource{ + // Normally we don’t mark username as sensitive data, but author of the ArgoCD extension have declared this property as sensitive + Schema: map[string]*schema.Schema{ + "username": { + Description: "The ArgoCD user name.", + Type: schema.TypeString, + Required: true, + Sensitive: true, + DiffSuppressFunc: tfhelper.DiffFuncSuppressSecretChanged, + }, + patHashKeyU: patHashSchemaU, + "password": { + Description: "The ArgoCD password.", + Type: schema.TypeString, + Required: true, + Sensitive: true, + DiffSuppressFunc: tfhelper.DiffFuncSuppressSecretChanged, + }, + patHashKeyP: patHashSchemaP, + }, + } + + r.Schema["authentication_token"] = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: at, + ExactlyOneOf: []string{"authentication_basic", "authentication_token"}, + } + + r.Schema["authentication_basic"] = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: aup, + } + + return r +} + +// Convert internal Terraform data structure to an AzDO data structure +func expandServiceEndpointArgoCD(d *schema.ResourceData) (*serviceendpoint.ServiceEndpoint, *uuid.UUID, error) { + serviceEndpoint, projectID := doBaseExpansion(d) + serviceEndpoint.Type = converter.String("argocd") + serviceEndpoint.Url = converter.String(d.Get("url").(string)) + authScheme := "Token" + + authParams := make(map[string]string) + + if x, ok := d.GetOk("authentication_token"); ok { + msi := x.([]interface{})[0].(map[string]interface{}) + authParams["apitoken"] = expandSecret(msi, "token") + } else if x, ok := d.GetOk("authentication_basic"); ok { + authScheme = "UsernamePassword" + msi := x.([]interface{})[0].(map[string]interface{}) + authParams["username"] = expandSecret(msi, "username") + authParams["password"] = expandSecret(msi, "password") + } + serviceEndpoint.Authorization = &serviceendpoint.EndpointAuthorization{ + Parameters: &authParams, + Scheme: &authScheme, + } + + return serviceEndpoint, projectID, nil +} + +// Convert AzDO data structure to internal Terraform data structure +func flattenServiceEndpointArgoCD(d *schema.ResourceData, serviceEndpoint *serviceendpoint.ServiceEndpoint, projectID *uuid.UUID) { + doBaseFlattening(d, serviceEndpoint, projectID) + if strings.EqualFold(*serviceEndpoint.Authorization.Scheme, "Token") { + auth := make(map[string]interface{}) + if x, ok := d.GetOk("authentication_token"); ok { + authList := x.([]interface{})[0].(map[string]interface{}) + if len(authList) > 0 { + newHash, hashKey := tfhelper.HelpFlattenSecretNested(d, "authentication_token", authList, "token") + auth[hashKey] = newHash + } + } + if serviceEndpoint.Authorization != nil && serviceEndpoint.Authorization.Parameters != nil { + auth["token"] = (*serviceEndpoint.Authorization.Parameters)["apitoken"] + } + d.Set("authentication_token", []interface{}{auth}) + } else if strings.EqualFold(*serviceEndpoint.Authorization.Scheme, "UsernamePassword") { + auth := make(map[string]interface{}) + if old, ok := d.GetOk("authentication_basic"); ok { + oldAuthList := old.([]interface{})[0].(map[string]interface{}) + if len(oldAuthList) > 0 { + newHash, hashKey := tfhelper.HelpFlattenSecretNested(d, "authentication_basic", oldAuthList, "password") + auth[hashKey] = newHash + newHash, hashKey = tfhelper.HelpFlattenSecretNested(d, "authentication_basic", oldAuthList, "username") + auth[hashKey] = newHash + } + } + if serviceEndpoint.Authorization != nil && serviceEndpoint.Authorization.Parameters != nil { + auth["password"] = (*serviceEndpoint.Authorization.Parameters)["password"] + auth["username"] = (*serviceEndpoint.Authorization.Parameters)["username"] + } + d.Set("authentication_basic", []interface{}{auth}) + } else { + panic(fmt.Errorf("inconsistent authorization scheme. Expected: (Token, UsernamePassword) , but got %s", *serviceEndpoint.Authorization.Scheme)) + } + + d.Set("url", *serviceEndpoint.Url) +} diff --git a/azuredevops/internal/service/serviceendpoint/resource_serviceendpoint_argocd_test.go b/azuredevops/internal/service/serviceendpoint/resource_serviceendpoint_argocd_test.go new file mode 100644 index 000000000..8574b7e4a --- /dev/null +++ b/azuredevops/internal/service/serviceendpoint/resource_serviceendpoint_argocd_test.go @@ -0,0 +1,225 @@ +//go:build (all || resource_serviceendpoint_argocd) && !exclude_serviceendpoints +// +build all resource_serviceendpoint_argocd +// +build !exclude_serviceendpoints + +package serviceendpoint + +import ( + "context" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/microsoft/azure-devops-go-api/azuredevops/v6/serviceendpoint" + "github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter" + "github.com/stretchr/testify/require" +) + +var argocdTestServiceEndpointIDpassword = uuid.New() +var argocdRandomServiceEndpointProjectIDpassword = uuid.New() +var argocdTestServiceEndpointProjectIDpassword = &argocdRandomServiceEndpointProjectIDpassword + +var argocdTestServiceEndpointPassword = serviceendpoint.ServiceEndpoint{ + Authorization: &serviceendpoint.EndpointAuthorization{ + Parameters: &map[string]string{ + "username": "AR_TEST_username", + "password": "AR_TEST_password", + }, + Scheme: converter.String("UsernamePassword"), + }, + Id: &argocdTestServiceEndpointIDpassword, + Name: converter.String("UNIT_TEST_CONN_NAME"), + Owner: converter.String("library"), // Supported values are "library", "agentcloud" + Type: converter.String("argocd"), + Url: converter.String("https://www.argocd.com"), + ServiceEndpointProjectReferences: &[]serviceendpoint.ServiceEndpointProjectReference{ + { + ProjectReference: &serviceendpoint.ProjectReference{ + Id: argocdTestServiceEndpointProjectIDpassword, + }, + Name: converter.String("UNIT_TEST_CONN_NAME"), + Description: converter.String("UNIT_TEST_CONN_DESCRIPTION"), + }, + }, +} + +var argocdTestServiceEndpointID = uuid.New() +var argocdRandomServiceEndpointProjectID = uuid.New() +var argocdTestServiceEndpointProjectID = &argocdRandomServiceEndpointProjectID + +var argocdTestServiceEndpoint = serviceendpoint.ServiceEndpoint{ + Authorization: &serviceendpoint.EndpointAuthorization{ + Parameters: &map[string]string{ + "apitoken": "AR_TEST_token", + }, + Scheme: converter.String("Token"), + }, + Id: &argocdTestServiceEndpointID, + Name: converter.String("UNIT_TEST_CONN_NAME"), + Owner: converter.String("library"), // Supported values are "library", "agentcloud" + Type: converter.String("argocd"), + Url: converter.String("https://www.argocd.com"), + ServiceEndpointProjectReferences: &[]serviceendpoint.ServiceEndpointProjectReference{ + { + ProjectReference: &serviceendpoint.ProjectReference{ + Id: argocdTestServiceEndpointProjectID, + }, + Name: converter.String("UNIT_TEST_CONN_NAME"), + Description: converter.String("UNIT_TEST_CONN_DESCRIPTION"), + }, + }, +} + +// verifies that the flatten/expand round trip yields the same service endpoint +func testServiceEndpointArgoCD_ExpandFlatten_Roundtrip(t *testing.T, ep *serviceendpoint.ServiceEndpoint, id *uuid.UUID) { + for _, ep := range []*serviceendpoint.ServiceEndpoint{ep, ep} { + + resourceData := schema.TestResourceDataRaw(t, ResourceServiceEndpointArgoCD().Schema, nil) + flattenServiceEndpointArgoCD(resourceData, ep, id) + + serviceEndpointAfterRoundTrip, projectID, err := expandServiceEndpointArgoCD(resourceData) + require.Nil(t, err) + require.Equal(t, *ep, *serviceEndpointAfterRoundTrip) + require.Equal(t, id, projectID) + + } +} +func TestServiceEndpointArgoCD_ExpandFlatten_RoundtripPassword(t *testing.T) { + testServiceEndpointArgoCD_ExpandFlatten_Roundtrip(t, &argocdTestServiceEndpointPassword, argocdTestServiceEndpointProjectIDpassword) +} + +func TestServiceEndpointArgoCD_ExpandFlatten_RoundtripToken(t *testing.T) { + testServiceEndpointArgoCD_ExpandFlatten_Roundtrip(t, &argocdTestServiceEndpoint, argocdTestServiceEndpointProjectID) +} + +// verifies that if an error is produced on create, the error is not swallowed +func testServiceEndpointArgoCD_Create_DoesNotSwallowError(t *testing.T, ep *serviceendpoint.ServiceEndpoint, id *uuid.UUID) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := ResourceServiceEndpointArgoCD() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointArgoCD(resourceData, ep, id) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &client.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.CreateServiceEndpointArgs{Endpoint: ep} + 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") +} +func TestServiceEndpointArgoCD_Create_DoesNotSwallowErrorToken(t *testing.T) { + testServiceEndpointArgoCD_Create_DoesNotSwallowError(t, &argocdTestServiceEndpoint, argocdTestServiceEndpointProjectID) +} +func TestServiceEndpointArgoCD_Create_DoesNotSwallowErrorPassword(t *testing.T) { + testServiceEndpointArgoCD_Create_DoesNotSwallowError(t, &argocdTestServiceEndpointPassword, argocdTestServiceEndpointProjectIDpassword) +} + +// verifies that if an error is produced on a read, it is not swallowed +func testServiceEndpointArgoCD_Read_DoesNotSwallowError(t *testing.T, ep *serviceendpoint.ServiceEndpoint, id *uuid.UUID) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := ResourceServiceEndpointArgoCD() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointArgoCD(resourceData, ep, id) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &client.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.GetServiceEndpointDetailsArgs{ + EndpointId: ep.Id, + Project: converter.String(id.String()), + } + 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") +} +func TestServiceEndpointArgoCD_Read_DoesNotSwallowErrorToken(t *testing.T) { + testServiceEndpointArgoCD_Read_DoesNotSwallowError(t, &argocdTestServiceEndpoint, argocdTestServiceEndpointProjectID) +} +func TestServiceEndpointArgoCD_Read_DoesNotSwallowErrorPassword(t *testing.T) { + testServiceEndpointArgoCD_Read_DoesNotSwallowError(t, &argocdTestServiceEndpointPassword, argocdTestServiceEndpointProjectIDpassword) +} + +// verifies that if an error is produced on a delete, it is not swallowed +func testServiceEndpointArgoCD_Delete_DoesNotSwallowError(t *testing.T, ep *serviceendpoint.ServiceEndpoint, id *uuid.UUID) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := ResourceServiceEndpointArgoCD() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointArgoCD(resourceData, ep, id) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &client.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.DeleteServiceEndpointArgs{ + EndpointId: ep.Id, + ProjectIds: &[]string{ + id.String(), + }, + } + 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") +} +func TestServiceEndpointArgoCD_Delete_DoesNotSwallowErrorToken(t *testing.T) { + testServiceEndpointArgoCD_Delete_DoesNotSwallowError(t, &argocdTestServiceEndpoint, argocdTestServiceEndpointProjectID) +} +func TestServiceEndpointArgoCD_Delete_DoesNotSwallowErrorPassword(t *testing.T) { + testServiceEndpointArgoCD_Delete_DoesNotSwallowError(t, &argocdTestServiceEndpointPassword, argocdTestServiceEndpointProjectIDpassword) +} + +// verifies that if an error is produced on an update, it is not swallowed +func testServiceEndpointArgoCD_Update_DoesNotSwallowError(t *testing.T, ep *serviceendpoint.ServiceEndpoint, id *uuid.UUID) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + r := ResourceServiceEndpointArgoCD() + resourceData := schema.TestResourceDataRaw(t, r.Schema, nil) + flattenServiceEndpointArgoCD(resourceData, ep, id) + + buildClient := azdosdkmocks.NewMockServiceendpointClient(ctrl) + clients := &client.AggregatedClient{ServiceEndpointClient: buildClient, Ctx: context.Background()} + + expectedArgs := serviceendpoint.UpdateServiceEndpointArgs{ + Endpoint: ep, + EndpointId: ep.Id, + } + + 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") +} +func TestServiceEndpointArgoCD_Update_DoesNotSwallowErrorToken(t *testing.T) { + testServiceEndpointArgoCD_Delete_DoesNotSwallowError(t, &argocdTestServiceEndpoint, argocdTestServiceEndpointProjectID) +} +func TestServiceEndpointArgoCD_Update_DoesNotSwallowErrorPassword(t *testing.T) { + testServiceEndpointArgoCD_Delete_DoesNotSwallowError(t, &argocdTestServiceEndpointPassword, argocdTestServiceEndpointProjectIDpassword) +} diff --git a/azuredevops/provider.go b/azuredevops/provider.go index f2f3e9462..b53d676b1 100644 --- a/azuredevops/provider.go +++ b/azuredevops/provider.go @@ -40,6 +40,7 @@ func Provider() *schema.Provider { "azuredevops_repository_policy_max_path_length": repository.ResourceRepositoryMaxPathLength(), "azuredevops_repository_policy_max_file_size": repository.ResourceRepositoryMaxFileSize(), "azuredevops_repository_policy_check_credentials": repository.ResourceRepositoryPolicyCheckCredentials(), + "azuredevops_serviceendpoint_argocd": serviceendpoint.ResourceServiceEndpointArgoCD(), "azuredevops_serviceendpoint_artifactory": serviceendpoint.ResourceServiceEndpointArtifactory(), "azuredevops_serviceendpoint_aws": serviceendpoint.ResourceServiceEndpointAws(), "azuredevops_serviceendpoint_azurerm": serviceendpoint.ResourceServiceEndpointAzureRM(), diff --git a/azuredevops/provider_test.go b/azuredevops/provider_test.go index a928b2aed..432daafed 100644 --- a/azuredevops/provider_test.go +++ b/azuredevops/provider_test.go @@ -31,6 +31,7 @@ func TestProvider_HasChildResources(t *testing.T) { "azuredevops_serviceendpoint_bitbucket", "azuredevops_serviceendpoint_kubernetes", "azuredevops_serviceendpoint_servicefabric", + "azuredevops_serviceendpoint_argocd", "azuredevops_serviceendpoint_aws", "azuredevops_serviceendpoint_artifactory", "azuredevops_serviceendpoint_sonarqube", diff --git a/website/azuredevops.erb b/website/azuredevops.erb index 963364c1b..9be47face 100644 --- a/website/azuredevops.erb +++ b/website/azuredevops.erb @@ -166,6 +166,9 @@
  • azuredevops_repository_policy_check_credentials
  • +
  • + azuredevops_serviceendpoint_argocd +
  • azuredevops_serviceendpoint_artifactory
  • diff --git a/website/docs/r/serviceendpoint_argocd.html.markdown b/website/docs/r/serviceendpoint_argocd.html.markdown new file mode 100644 index 000000000..e157ca094 --- /dev/null +++ b/website/docs/r/serviceendpoint_argocd.html.markdown @@ -0,0 +1,89 @@ +--- +layout: "azuredevops" +page_title: "AzureDevops: azuredevops_serviceendpoint_argocd" +description: |- + Manages a ArgoCD server endpoint within Azure DevOps organization. +--- + +# azuredevops_serviceendpoint_argocd +Manages a ArgoCD service endpoint within Azure DevOps. Using this service endpoint requires you to first install [Argo CD Extension](https://marketplace.visualstudio.com/items?itemName=scb-tomasmortensen.vsix-argocd). + +## Example Usage + +```hcl +resource "azuredevops_project" "project" { + name = "Sample Project" + visibility = "private" + version_control = "Git" + work_item_template = "Agile" +} + +resource "azuredevops_serviceendpoint_argocd" "serviceendpoint" { + + project_id = azuredevops_project.project.id + service_endpoint_name = "Sample ArgoCD" + description = "Managed by Terraform" + url = "https://argocd.my.com" + authentication_token { + token = "0000000000000000000000000000000000000000" + } +} +``` +Alternatively a username and password may be used. + +```hcl +resource "azuredevops_serviceendpoint_argocd" "serviceendpoint" { + + project_id = azuredevops_project.project.id + service_endpoint_name = "Sample ArgoCD" + description = "Managed by Terraform" + url = "https://argocd.my.com" + authentication_basic { + username = "sampleuser" + password = "0000000000000000000000000000000000000000" + } +} +``` +## Argument Reference + +The following arguments are supported: + +- `project_id` - (Required) The project ID or project name. +- `service_endpoint_name` - (Required) The Service Endpoint name. +- `url` - (Required) URL of the ArgoCD server to connect with. +- `description` - (Optional) The Service Endpoint description. +- `authentication_token` - (Optional) An `authentication_token` block for the ArgoCD as documented below. +- `authentication_basic` - (Optional) An `authentication_basic` block for the ArgoCD as documented below. + +~> **NOTE:** `authentication_basic` and `authentication_token` conflict with each other, only one is required. + +--- + +A `authentication_token` block supports the following: + + - `token` - Authentication Token generated through ArgoCD. + +A `authentication_basic` block supports the following: + - `username` - ArgoCD Username. + - `password` - ArgoCD Password. + +## 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 Connections](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml) +- [ArgoCD Project/User Token](https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_account_generate-token/) +- [Argo CD Extension](https://marketplace.visualstudio.com/items?itemName=scb-tomasmortensen.vsix-argocd) + +## Import +Azure DevOps Service Endpoint ArgoCD can be imported using the **projectID/serviceEndpointID**, e.g. + + +```shell +$ terraform import azuredevops_serviceendpoint_argocd.serviceendpoint 00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000000 +```