diff --git a/go.mod b/go.mod index 3cf78f5f1..b9069a144 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/antihax/optional v1.0.0 github.com/aws/aws-sdk-go v1.46.4 github.com/docker/docker v24.0.5+incompatible - github.com/harness/harness-go-sdk v0.4.10 + github.com/harness/harness-go-sdk v0.4.11 github.com/harness/harness-openapi-go-client v0.0.21 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 diff --git a/go.sum b/go.sum index 0d379a0e3..fd03df7fa 100644 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/harness/harness-go-sdk v0.4.10 h1:iRpsG35I1bZ618FOHnAMKG7FBfiEpTGPyLz0mICsuAU= -github.com/harness/harness-go-sdk v0.4.10/go.mod h1:a/1HYTgVEuNEoh3Z3IsOHZdlUNxl94KcX57ZSNVGll0= +github.com/harness/harness-go-sdk v0.4.11 h1:AJ9t7Yh9Ub3SnlDmq/3X4AosCStKBJczWPYVt46WODw= +github.com/harness/harness-go-sdk v0.4.11/go.mod h1:a/1HYTgVEuNEoh3Z3IsOHZdlUNxl94KcX57ZSNVGll0= github.com/harness/harness-openapi-go-client v0.0.21 h1:VtJnpQKZvCAlaCmUPbNR69OT3c5WRdhNN5TOgUwtwZ4= github.com/harness/harness-openapi-go-client v0.0.21/go.mod h1:u0vqYb994BJGotmEwJevF4L3BNAdU9i8ui2d22gmLPA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8326f8552..7e84f831f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -83,6 +83,7 @@ import ( "github.com/harness/terraform-provider-harness/internal/service/platform/pipeline" "github.com/harness/terraform-provider-harness/internal/service/platform/pipeline_filters" "github.com/harness/terraform-provider-harness/internal/service/platform/project" + pl_provider "github.com/harness/terraform-provider-harness/internal/service/platform/provider" "github.com/harness/terraform-provider-harness/internal/service/platform/repo" "github.com/harness/terraform-provider-harness/internal/service/platform/resource_group" "github.com/harness/terraform-provider-harness/internal/service/platform/role_assignments" @@ -203,6 +204,7 @@ func Provider(version string) func() *schema.Provider { "harness_platform_environment_clusters_mapping": pl_environment_clusters_mapping.DataSourceEnvironmentClustersMapping(), "harness_platform_environment_service_overrides": pl_environment_service_overrides.DataSourceEnvironmentServiceOverrides(), "harness_platform_service_overrides_v2": pl_service_overrides_v2.DataSourceServiceOverrides(), + "harness_platform_provider": pl_provider.DataSourceProvider(), "harness_platform_overrides": pl_overrides.DataSourceOverrides(), "harness_platform_gitops_agent": gitops_agent.DataSourceGitopsAgent(), "harness_platform_gitops_agent_deploy_yaml": agent_yaml.DataSourceGitopsAgentDeployYaml(), @@ -338,6 +340,7 @@ func Provider(version string) func() *schema.Provider { "harness_platform_feature_flag_target_group": feature_flag_target_group.ResourceFeatureFlagTargetGroup(), "harness_platform_feature_flag_target": feature_flag_target.ResourceFeatureFlagTarget(), "harness_platform_service_overrides_v2": pl_service_overrides_v2.ResourceServiceOverrides(), + "harness_platform_provider": pl_provider.ResourceProvider(), "harness_platform_overrides": pl_overrides.ResourceOverrides(), "harness_platform_ff_api_key": ff_api_key.ResourceFFApiKey(), "harness_platform_gitops_agent": gitops_agent.ResourceGitopsAgent(), diff --git a/internal/service/platform/provider/data_source_provider.go b/internal/service/platform/provider/data_source_provider.go new file mode 100644 index 000000000..cca3b9850 --- /dev/null +++ b/internal/service/platform/provider/data_source_provider.go @@ -0,0 +1,47 @@ +package provider + +import ( + "context" + + "github.com/harness/terraform-provider-harness/helpers" + "github.com/harness/terraform-provider-harness/internal" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func DataSourceProvider() *schema.Resource { + resource := &schema.Resource{ + Description: "Data source for Harness Provider.", + + ReadContext: dataSourceProviderRead, + + Schema: map[string]*schema.Schema{ + "identifier": { + Description: "The identifier of the provider entity.", + Type: schema.TypeString, + Required: true, + }, + }, + } + return resource +} + +func dataSourceProviderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + identifier := d.Get("identifier").(string) + + resp, httpResp, err := c.ProviderApi.GetProvider(ctx, identifier, c.AccountId) + if err != nil { + return helpers.HandleApiError(err, d, httpResp) + } + + if resp.Data == nil { + d.SetId("") + d.MarkNewResource() + return nil + } + + readProvider(d, resp.Data) + + return nil +} diff --git a/internal/service/platform/provider/data_source_provider_test.go b/internal/service/platform/provider/data_source_provider_test.go new file mode 100644 index 000000000..ece11c6fb --- /dev/null +++ b/internal/service/platform/provider/data_source_provider_test.go @@ -0,0 +1,51 @@ +package provider_test + +import ( + "fmt" + "testing" + + "github.com/harness/harness-go-sdk/harness/utils" + "github.com/harness/terraform-provider-harness/internal/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestDataSourceProvider(t *testing.T) { + + id := fmt.Sprintf("%s_%s", t.Name(), utils.RandStringBytes(6)) + name := id + resourceName := "data.harness_platform_provider.test" + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceProvider(id, name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "identifier", id), + ), + }, + }, + }) +} + +func testAccDataSourceProvider(id string, name string) string { + return fmt.Sprintf(` + resource "harness_platform_provider" "test" { + identifier = "%[1]s" + name = "%[2]s" + spec { + type = "BITBUCKET_SERVER" + domain = "https://example.com" + secret_manager_ref = "secret-ref" + delegate_selectors = ["delegate-1", "delegate-2"] + client_id = "client-id" + client_secret_ref = "client-secret-ref" + } + } + + data "harness_platform_provider" "test" { + identifier = harness_platform_provider.test.identifier + } +`, id, name) +} diff --git a/internal/service/platform/provider/resource_provider.go b/internal/service/platform/provider/resource_provider.go new file mode 100644 index 000000000..180787705 --- /dev/null +++ b/internal/service/platform/provider/resource_provider.go @@ -0,0 +1,293 @@ +package provider + +import ( + "context" + "encoding/json" + "log" + "net/http" + + "github.com/antihax/optional" + "github.com/harness/harness-go-sdk/harness/nextgen" + "github.com/harness/terraform-provider-harness/helpers" + "github.com/harness/terraform-provider-harness/internal" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func ResourceProvider() *schema.Resource { + resource := &schema.Resource{ + Description: "Resource for creating a Harness Provider.", + + ReadContext: resourceProviderRead, + UpdateContext: resourceProviderCreateOrUpdate, + DeleteContext: resourceProviderDelete, + CreateContext: resourceProviderCreateOrUpdate, + Importer: helpers.AccountLevelResourceImporter, + + Schema: map[string]*schema.Schema{ + "identifier": { + Description: "The identifier of the provider entity.", + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + "name": { + Description: "The name of the provider entity.", + Type: schema.TypeString, + Required: true, + }, + "description": { + Description: "The description of the provider entity.", + Type: schema.TypeString, + Optional: true, + Required: false, + Computed: false, + }, + "type": { + Description: "The type of the provider entity.", + Type: schema.TypeString, + Optional: false, + Computed: true, + }, + "last_modified_at": { + Description: "The last modified time of the provider entity.", + Type: schema.TypeInt, + Optional: false, + Computed: true, + }, + "spec": { + Description: "Contains parameters related to the provider entity.", + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Description: "The type of the provider entity.", + Type: schema.TypeString, + Required: true, + }, + "domain": { + Description: "Host domain of the provider.", + Type: schema.TypeString, + Optional: true, + Computed: false, + Required: false, + }, + "secret_manager_ref": { + Description: "Secret Manager Ref to store the access/refresh tokens", + Type: schema.TypeString, + Optional: true, + Computed: false, + Required: false, + }, + "delegate_selectors": { + Description: "Delegate selectors to fetch the access token", + Type: schema.TypeList, + Optional: true, + Computed: false, + Required: false, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "client_id": { + Description: "Client Id of the OAuth app to connect", + Type: schema.TypeString, + Optional: true, + Computed: false, + Required: false, + }, + "client_secret_ref": { + Description: "Client Secret Ref of the OAuth app to connect", + Type: schema.TypeString, + Optional: true, + Computed: false, + Required: false, + }, + }, + }, + }, + }, + } + + return resource +} + +func resourceProviderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + + id := d.Id() + + resp, httpResp, err := c.ProviderApi.GetProvider(ctx, id, c.AccountId) + + if err != nil { + return helpers.HandleReadApiError(err, d, httpResp) + } + + readProvider(d, resp.Data) + + return nil +} + +func resourceProviderCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + + var err error + var httpResp *http.Response + + id := d.Id() + + if id == "" { + var resp nextgen.ProviderCreateResponse + provider := createProvider(d) + providerParams := providerCreateParam(provider) + resp, httpResp, err = c.ProviderApi.CreateProvider(ctx, c.AccountId, &providerParams) + + if err != nil { + return helpers.HandleApiError(err, d, httpResp) + } + + readCreateProvider(d, resp.Data) + } else { + var resp nextgen.ProviderUpdateResponse + provider := updateProvider(d) + providerParams := providerUpdateParam(provider) + resp, httpResp, err = c.ProviderApi.UpdateProvider(ctx, id, c.AccountId, &providerParams) + + if err != nil { + return helpers.HandleApiError(err, d, httpResp) + } + + readUpdateProvider(d, resp.Data) + } + return nil +} + +func resourceProviderDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var resp nextgen.ProviderDeleteResponse + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + + resp, httpResp, err := c.ProviderApi.DeleteProvider(ctx, d.Id(), c.AccountId) + if err != nil { + return helpers.HandleApiError(err, d, httpResp) + } + + readDeleteProvider(d, resp.Data) + return nil +} + +func createProvider(d *schema.ResourceData) *nextgen.ProviderCreateRequest { + specData := d.Get("spec").([]interface{}) + var spec json.RawMessage + + if len(specData) > 0 { + specMap := specData[0].(map[string]interface{}) + + delegateInterfaces, ok := specMap["delegate_selectors"].([]interface{}) + if !ok { + log.Fatalf("delegate_selectors is not a []interface{}") + } + + var delegateSelectors []string + for _, v := range delegateInterfaces { + str, ok := v.(string) + if !ok { + log.Fatalf("One of the delegate_selectors is not a string") + } + delegateSelectors = append(delegateSelectors, str) + } + + if specMap["type"] == "BITBUCKET_SERVER" { + bitbucketSpec := &nextgen.BitbucketServerSpec{ + Domain: specMap["domain"].(string), + DelegateSelectors: delegateSelectors, + SecretManagerRef: specMap["secret_manager_ref"].(string), + ClientId: specMap["client_id"].(string), + ClientSecretRef: specMap["client_secret_ref"].(string), + Type_: nextgen.ProviderTypes.BitbucketServer, + } + spec, _ = json.Marshal(bitbucketSpec) + } + + } + + return &nextgen.ProviderCreateRequest{ + Identifier: d.Get("identifier").(string), + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Spec: spec, + } +} + +func updateProvider(d *schema.ResourceData) *nextgen.ProviderUpdateRequest { + specData := d.Get("spec").([]interface{}) + var spec json.RawMessage + + if len(specData) > 0 { + specMap := specData[0].(map[string]interface{}) + marshalledSpec, err := json.Marshal(specMap) + if err != nil { + log.Printf("Error marshaling spec: %v", err) + return nil + } + + spec = marshalledSpec + } + + return &nextgen.ProviderUpdateRequest{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Spec: spec, + } +} + +func readCreateProvider(d *schema.ResourceData, provider *nextgen.ProviderCreateApiResponse) { + d.SetId(provider.Identifier) + d.Set("identifier", provider.Identifier) +} + +func readUpdateProvider(d *schema.ResourceData, provider *nextgen.ProviderUpdateApiResponse) { + d.SetId(provider.Identifier) + d.Set("identifier", provider.Identifier) +} + +func readDeleteProvider(d *schema.ResourceData, provider *nextgen.ProviderDeleteApiResponse) { + d.SetId(provider.Identifier) + d.Set("identifier", provider.Identifier) +} + +func providerCreateParam(provider *nextgen.ProviderCreateRequest) nextgen.ProviderApiCreateProviderOpts { + return nextgen.ProviderApiCreateProviderOpts{ + Body: optional.NewInterface(provider), + } +} + +func providerUpdateParam(provider *nextgen.ProviderUpdateRequest) nextgen.ProviderApiUpdateProviderOpts { + return nextgen.ProviderApiUpdateProviderOpts{ + Body: optional.NewInterface(provider), + } +} + +func readProvider(d *schema.ResourceData, so *nextgen.Provider) { + d.SetId(so.Identifier) + d.Set("identifier", so.Identifier) + d.Set("name", so.Name) + d.Set("description", so.Description) + d.Set("type", so.Type_) + d.Set("last_modified_at", so.LastModifiedAt) + var specData map[string]interface{} + err := json.Unmarshal(so.Spec, &specData) + if err != nil { + log.Printf("Error unmarshalling JSON: %v", err) + return + } + d.Set("spec", []interface{}{ + map[string]interface{}{ + "type": so.Type_, + "domain": specData["domain"], + "secret_manager_ref": specData["secretManagerRef"], + "client_id": specData["clientId"], + "client_secret_ref": specData["clientSecretRef"], + "delegate_selectors": specData["delegateSelectors"], + }, + }) +} diff --git a/internal/service/platform/provider/resource_provider_test.go b/internal/service/platform/provider/resource_provider_test.go new file mode 100644 index 000000000..8c915aad7 --- /dev/null +++ b/internal/service/platform/provider/resource_provider_test.go @@ -0,0 +1,93 @@ +package provider_test + +import ( + "fmt" + "testing" + + "github.com/harness/harness-go-sdk/harness/nextgen" + "github.com/harness/harness-go-sdk/harness/utils" + "github.com/harness/terraform-provider-harness/internal/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestResourceProvider(t *testing.T) { + + name := t.Name() + id := fmt.Sprintf("%s_%s", name, utils.RandStringBytes(5)) + updatedName := fmt.Sprintf("%s_updated", name) + resourceName := "harness_platform_provider.test" + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testProviderDestroy(resourceName), + Steps: []resource.TestStep{ + { + Config: testResourceProvider(id, name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", id), + resource.TestCheckResourceAttr(resourceName, "name", name), + ), + }, + { + Config: testResourceProvider(id, updatedName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", id), + resource.TestCheckResourceAttr(resourceName, "name", updatedName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: acctest.AccountLevelResourceImportStateIdFunc(resourceName), + }, + }, + }) +} + +func testGetProvider(resourceName string, state *terraform.State) (*nextgen.Provider, error) { + r := acctest.TestAccGetResource(resourceName, state) + c, ctx := acctest.TestAccGetPlatformClientWithContext() + id := r.Primary.ID + + resp, _, err := c.ProviderApi.GetProvider(ctx, id, c.AccountId) + if err != nil { + return nil, err + } + + if resp.Data == nil { + return nil, nil + } + + return resp.Data, nil +} + +func testProviderDestroy(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + provider, _ := testGetProvider(resourceName, state) + if provider != nil { + return fmt.Errorf("found provider: %s", provider.Identifier) + } + + return nil + } +} + +func testResourceProvider(id string, name string) string { + return fmt.Sprintf(` + resource "harness_platform_provider" "test" { + identifier = "%[1]s" + name = "%[2]s" + spec { + type = "BITBUCKET_SERVER" + domain = "https://example.com" + secret_manager_ref = "secret-ref" + delegate_selectors = ["delegate-1", "delegate-2"] + client_id = "client-id" + client_secret_ref = "client-secret-ref" + } + } +`, id, name) +}