diff --git a/.changelog/660.txt b/.changelog/660.txt new file mode 100644 index 000000000..b4d70a22a --- /dev/null +++ b/.changelog/660.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +resource_feature_flag_target - Added feature flag target resources to the Harness Terraform Provider. +``` \ No newline at end of file diff --git a/docs/resources/platform_feature_flag_target.md b/docs/resources/platform_feature_flag_target.md new file mode 100644 index 000000000..880e71e93 --- /dev/null +++ b/docs/resources/platform_feature_flag_target.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "harness_platform_feature_flag_target Resource - terraform-provider-harness" +subcategory: "Next Gen" +description: |- + Resource for managing Feature Flag Targets. +--- + +# harness_platform_feature_flag_target (Resource) + +Resource for managing Feature Flag Targets. + +## Example Usage + +```terraform +resource "harness_platform_feature_flag_target" "target" { + org_id = "test" + project_id = "test" + + identifier = "MY_FEATURE" + environment = "MY_ENVIRONMENT" + name = "MY_FEATURE" + account_id = "MY_ACCOUNT_ID" + attributes = { "foo" : "bar" } +} +``` + + +## Schema + +### Required + +- `account_id` (String) Account Identifier +- `environment` (String) Environment Identifier +- `identifier` (String) Identifier of the Feature Flag Target +- `name` (String) Target Name +- `org_id` (String) Organization Identifier +- `project_id` (String) Project Identifier + +### Optional + +- `attributes` (Map of String) Attributes + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/harness_platform_feature_flag/resource.tf b/examples/resources/harness_platform_feature_flag/resource.tf index 45c143948..8730097e3 100644 --- a/examples/resources/harness_platform_feature_flag/resource.tf +++ b/examples/resources/harness_platform_feature_flag/resource.tf @@ -1,63 +1,63 @@ // Boolean Flag resource "harness_platform_feature_flag" "mybooleanflag" { - org_id = "test" + org_id = "test" project_id = "testff" - kind = "boolean" - name = "MY_FEATURE" + kind = "boolean" + name = "MY_FEATURE" identifier = "MY_FEATURE" - permanent = false + permanent = false - default_on_variation = "Enabled" + default_on_variation = "Enabled" default_off_variation = "Disabled" variation { - identifier = "Enabled" - name = "Enabled" + identifier = "Enabled" + name = "Enabled" description = "The feature is enabled" - value = "true" + value = "true" } variation { - identifier = "Disabled" - name = "Disabled" + identifier = "Disabled" + name = "Disabled" description = "The feature is disabled" - value = "false" + value = "false" } } // Multivariate flag resource "harness_platform_feature_flag" "mymultivariateflag" { - org_id = "test" + org_id = "test" project_id = "testff" - kind = "int" - name = "FREE_TRIAL_DURATION" + kind = "int" + name = "FREE_TRIAL_DURATION" identifier = "FREE_TRIAL_DURATION" - permanent = false + permanent = false - default_on_variation = "trial7" + default_on_variation = "trial7" default_off_variation = "trial20" variation { - identifier = "trial7" - name = "7 days trial" + identifier = "trial7" + name = "7 days trial" description = "Free trial period 7 days" - value = "7" + value = "7" } variation { - identifier = "trial14" - name = "14 days trial" + identifier = "trial14" + name = "14 days trial" description = "Free trial period 14 days" - value = "14" + value = "14" } variation { - identifier = "trial20" - name = "20 days trial" + identifier = "trial20" + name = "20 days trial" description = "Free trial period 20 days" - value = "20" + value = "20" } } \ No newline at end of file diff --git a/examples/resources/harness_platform_feature_flag_target/resource.tf b/examples/resources/harness_platform_feature_flag_target/resource.tf new file mode 100644 index 000000000..e10acf335 --- /dev/null +++ b/examples/resources/harness_platform_feature_flag_target/resource.tf @@ -0,0 +1,10 @@ +resource "harness_platform_feature_flag_target" "target" { + org_id = "test" + project_id = "test" + + identifier = "MY_FEATURE" + environment = "MY_ENVIRONMENT" + name = "MY_FEATURE" + account_id = "MY_ACCOUNT_ID" + attributes = { "foo" : "bar" } +} diff --git a/examples/resources/harness_platform_ff_api_key/resource.tf b/examples/resources/harness_platform_ff_api_key/resource.tf index c528271ba..25931aee7 100644 --- a/examples/resources/harness_platform_ff_api_key/resource.tf +++ b/examples/resources/harness_platform_ff_api_key/resource.tf @@ -1,15 +1,15 @@ resource "harness_platform_ff_api_key" "testserverapikey" { - identifier = "testserver" - name = "TestServer" - description = "this is a server SDK key" - org_id = "test" - project_id = "testff" - env_id = "testenv" - expired_at = 1713729225 - type = "Server" + identifier = "testserver" + name = "TestServer" + description = "this is a server SDK key" + org_id = "test" + project_id = "testff" + env_id = "testenv" + expired_at = 1713729225 + type = "Server" } output "serversdkkey" { - value = harness_platform_ff_api_key.testserverapikey.api_key + value = harness_platform_ff_api_key.testserverapikey.api_key sensitive = true } \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 553522bd5..e16033fa6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -6,6 +6,7 @@ import ( "log" "github.com/harness/terraform-provider-harness/internal/service/platform/feature_flag" + "github.com/harness/terraform-provider-harness/internal/service/platform/feature_flag_target" "github.com/harness/terraform-provider-harness/internal/service/platform/ff_api_key" "github.com/harness/terraform-provider-harness/internal/service/platform/gitops/agent_yaml" "github.com/harness/terraform-provider-harness/internal/service/platform/manual_freeze" @@ -266,8 +267,9 @@ func Provider(version string) func() *schema.Provider { "harness_platform_environment_group": pl_environment_group.ResourceEnvironmentGroup(), "harness_platform_environment_clusters_mapping": pl_environment_clusters_mapping.ResourceEnvironmentClustersMapping(), "harness_platform_environment_service_overrides": pl_environment_service_overrides.ResourceEnvironmentServiceOverrides(), - "harness_platform_service_overrides_v2": pl_service_overrides_v2.ResourceServiceOverrides(), "harness_platform_feature_flag": feature_flag.ResourceFeatureFlag(), + "harness_platform_feature_flag_target": feature_flag_target.ResourceFeatureFlagTarget(), + "harness_platform_service_overrides_v2": pl_service_overrides_v2.ResourceServiceOverrides(), "harness_platform_ff_api_key": ff_api_key.ResourceFFApiKey(), "harness_platform_gitops_agent": gitops_agent.ResourceGitopsAgent(), "harness_platform_gitops_applications": gitops_applications.ResourceGitopsApplication(), diff --git a/internal/service/platform/feature_flag/resource_feature_flag.go b/internal/service/platform/feature_flag/resource_feature_flag.go index 337e487ad..ec57f103d 100644 --- a/internal/service/platform/feature_flag/resource_feature_flag.go +++ b/internal/service/platform/feature_flag/resource_feature_flag.go @@ -2,7 +2,7 @@ package feature_flag import ( "context" - "io/ioutil" + "io" "net/http" "strings" "time" @@ -209,7 +209,7 @@ func resourceFeatureFlagCreate(ctx context.Context, d *schema.ResourceData, meta resp, httpResp, err = c.FeatureFlagsApi.GetFeatureFlag(ctx, id, c.AccountId, qp.OrganizationId, qp.ProjectId, readOpts) if err != nil { - body, _ := ioutil.ReadAll(httpResp.Body) + body, _ := io.ReadAll(httpResp.Body) return diag.Errorf("readstatus: %s, \nBody:%s", httpResp.Status, body) //return helpers.HandleReadApiError(err, d, httpResp) } diff --git a/internal/service/platform/feature_flag_target/resource_feature_flag_target.go b/internal/service/platform/feature_flag_target/resource_feature_flag_target.go new file mode 100644 index 000000000..eae17ca12 --- /dev/null +++ b/internal/service/platform/feature_flag_target/resource_feature_flag_target.go @@ -0,0 +1,235 @@ +package feature_flag_target + +import ( + "context" + "io" + "net/http" + "time" + + "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 ResourceFeatureFlagTarget() *schema.Resource { + resource := &schema.Resource{ + Description: "Resource for managing Feature Flag Targets.", + + ReadContext: resourceFeatureFlagTargetRead, + DeleteContext: resourceFeatureFlagTargetDelete, + CreateContext: resourceFeatureFlagTargetCreateOrUpdate, + UpdateContext: resourceFeatureFlagTargetUpdate, + Importer: helpers.ProjectResourceImporter, + + Schema: map[string]*schema.Schema{ + "identifier": { + Description: "Identifier of the Feature Flag Target", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "org_id": { + Description: "Organization Identifier", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project_id": { + Description: "Project Identifier", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "environment": { + Description: "Environment Identifier", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "account_id": { + Description: "Account Identifier", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Description: "Target Name", + Type: schema.TypeString, + Required: true, + }, + "attributes": { + Description: "Attributes", + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } + + return resource +} + +// FFTargetQueryParameters is the query parameters for the feature flag target +type FFTargetQueryParameters struct { + Identifier string + OrganizationID string + ProjectID string + AccountID string + Environment string +} + +// FFTargetOpts is the options for the feature flag target +type FFTargetOpts struct { + Name string + Atributes map[string]interface{} +} + +// resourceFeatureFlagTargetRead is the read function for the feature flag target +func resourceFeatureFlagTargetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + id := d.Id() + + if id == "" { + d.MarkNewResource() + return nil + } + + qp := buildFFTargetQueryParameters(d) + + resp, httpResp, err := c.TargetsApi.GetTarget(ctx, id, c.AccountId, qp.OrganizationID, qp.ProjectID, qp.Environment) + + if err != nil { + return helpers.HandleReadApiError(err, d, httpResp) + } + + readFeatureFlagTarget(d, &resp, *qp) + + return nil +} + +// resourceFeatureFlagTargetDelete is the delete function for the feature flag target +func resourceFeatureFlagTargetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + + id := d.Id() + if id == "" { + return nil + } + + qp := buildFFTargetQueryParameters(d) + + httpResp, err := c.TargetsApi.DeleteTarget(ctx, id, c.AccountId, qp.OrganizationID, qp.ProjectID, qp.Environment) + if err != nil { + return helpers.HandleApiError(err, d, httpResp) + } + return nil +} + +// resourceFeatureFlagTargetCreateOrUpdate is the create function for the feature flag target +func resourceFeatureFlagTargetCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + + var err error + var httpResp *http.Response + target := buildFFTargetCreate(d) + qp := buildFFTargetQueryParameters(d) + id := d.Id() + + if id == "" { + httpResp, err = c.TargetsApi.CreateTarget(ctx, target, c.AccountId, qp.OrganizationID) + } else { + target, httpResp, err = c.TargetsApi.ModifyTarget(ctx, target, c.AccountId, qp.OrganizationID, qp.ProjectID, qp.Environment, id) + } + + if err != nil { + return helpers.HandleApiError(err, d, httpResp) + } + + readFeatureFlagTarget(d, &target, *qp) + + return nil +} + +// resourceFeatureFlagTargetUpdate is the update function for the feature flag target +func resourceFeatureFlagTargetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx) + + id := d.Id() + if id == "" { + id = d.Get("identifier").(string) + d.MarkNewResource() + } + + qp := buildFFTargetQueryParameters(d) + opts := buildFFTargetPatchOpts(d) + + var err error + var target nextgen.Target + var httpResp *http.Response + + target, httpResp, err = c.TargetsApi.PatchTarget(ctx, c.AccountId, qp.OrganizationID, qp.ProjectID, qp.Environment, id, opts) + if err != nil { + body, _ := io.ReadAll(httpResp.Body) + return diag.Errorf("readstatus: %s, \nBody:%s", httpResp.Status, body) + } + + readFeatureFlagTarget(d, &target, *qp) + + return nil +} + +// readFeatureFlagTarget is the read function for the feature flag target +func readFeatureFlagTarget(d *schema.ResourceData, flag *nextgen.Target, qp FFTargetQueryParameters) { + d.SetId(qp.Identifier) + d.Set("identifier", qp.Identifier) + d.Set("org_id", qp.OrganizationID) + d.Set("project_id", qp.ProjectID) + d.Set("environment", qp.Environment) + d.Set("account_id", qp.AccountID) + d.Set("name", flag.Name) + d.Set("attributes", flag.Attributes) +} + +// buildFFTargetQueryParameters is the query parameters for the feature flag target +func buildFFTargetQueryParameters(d *schema.ResourceData) *FFTargetQueryParameters { + return &FFTargetQueryParameters{ + Identifier: d.Get("identifier").(string), + OrganizationID: d.Get("org_id").(string), + ProjectID: d.Get("project_id").(string), + AccountID: d.Get("account_id").(string), + Environment: d.Get("environment").(string), + } +} + +// buildFFTargetCreateOpts +func buildFFTargetCreate(d *schema.ResourceData) nextgen.Target { + attribute := d.Get("attributes") + return nextgen.Target{ + Account: d.Get("account_id").(string), + Attributes: &attribute, + Environment: d.Get("environment").(string), + Identifier: d.Get("identifier").(string), + Org: d.Get("org_id").(string), + Name: d.Get("name").(string), + Project: d.Get("project_id").(string), + CreatedAt: time.Now().Unix(), + } +} + +// buildFFTargetPatchOpts is the options for the feature flag target +func buildFFTargetPatchOpts(d *schema.ResourceData) *nextgen.TargetsApiPatchTargetOpts { + opts := &FFTargetOpts{ + Name: d.Get("name").(string), + Atributes: d.Get("attributes").(map[string]interface{}), + } + + return &nextgen.TargetsApiPatchTargetOpts{ + Body: optional.NewInterface(opts), + } +} diff --git a/internal/service/platform/feature_flag_target/resource_feature_flag_target_test.go b/internal/service/platform/feature_flag_target/resource_feature_flag_target_test.go new file mode 100644 index 000000000..1c28ed0a2 --- /dev/null +++ b/internal/service/platform/feature_flag_target/resource_feature_flag_target_test.go @@ -0,0 +1,166 @@ +package feature_flag_target_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 TestAccResourceFeatureFlagTarget(t *testing.T) { + + name := t.Name() + targetName := name + id := fmt.Sprintf("%s_%s", name, utils.RandStringBytes(5)) + resourceName := "harness_platform_environment.test" + environment := "qa" + environmentId := fmt.Sprintf("%s_%s", "env", utils.RandStringBytes(5)) + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccResourceFeatureFlagTargetDestroy(resourceName), + Steps: []resource.TestStep{ + { + Config: testAccResourceFeatureFlagTarget(id, name, targetName, environmentId, environment), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "identifier", id), + resource.TestCheckResourceAttr(resourceName, "org_id", id), + resource.TestCheckResourceAttr(resourceName, "project_id", id), + resource.TestCheckResourceAttr(resourceName, "name", targetName), + ), + }, + { + Config: testAccResourceFeatureFlagTarget(id, name, name, environmentId, environment), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "identifier", id), + resource.TestCheckResourceAttr(resourceName, "org_id", id), + resource.TestCheckResourceAttr(resourceName, "project_id", id), + resource.TestCheckResourceAttr(resourceName, "name", name), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"yaml"}, + ImportStateIdFunc: acctest.ProjectResourceImportStateIdFunc(resourceName), + }, + }, + }) +} + +func testAccResourceFeatureFlagTarget(id string, name string, updatedName string, environmentId string, environment string) string { + return fmt.Sprintf(` + resource "harness_platform_organization" "test" { + identifier = "%[1]s" + name = "%[2]s" + } + + resource "harness_platform_project" "test" { + identifier = "%[1]s" + name = "%[2]s" + org_id = harness_platform_organization.test.id + color = "#0063F7" + } + + resource "harness_platform_environment" "test" { + identifier = "%[1]s" + name = "%[2]s" + org_id = harness_platform_project.test.org_id + project_id = harness_platform_project.test.id + tags = ["foo:bar", "baz"] + type = "PreProduction" + yaml = <<-EOT + environment: + name: %[2]s + identifier: %[1]s + orgIdentifier: ${harness_platform_project.test.org_id} + projectIdentifier: ${harness_platform_project.test.id} + type: PreProduction + tags: + foo: bar + baz: "" + variables: + - name: envVar1 + type: String + value: v1 + description: "" + - name: envVar2 + type: String + value: v2 + description: "" + overrides: + manifests: + - manifest: + identifier: manifestEnv + type: Values + spec: + store: + type: Git + spec: + connectorRef: <+input> + gitFetchType: Branch + paths: + - file1 + repoName: <+input> + branch: master + configFiles: + - configFile: + identifier: configFileEnv + spec: + store: + type: Harness + spec: + files: + - account:/Add-ons/svcOverrideTest + secretFiles: [] + EOT + } + + resource "harness_platform_feature_flag_target" "test" { + identifier = "%[1]s" + org_id = harness_platform_project.test.org_id + project_id = harness_platform_project.test.id + environment = harness_platform_environment.test.id + account_id = harness_platform_project.test.id + name = "%[2]s" + attributes = {} + } +`, id, name, updatedName, environmentId, environment) +} + +func testAccResourceFeatureFlagTargetDestroy(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + env, _ := testAccGetPlatformFeatureFlagTarget(resourceName, state) + if env != nil { + return fmt.Errorf("Feature Flag Target not found: %s", env.Identifier) + } + + return nil + } +} + +func testAccGetPlatformFeatureFlagTarget(resourceName string, state *terraform.State) (*nextgen.Target, error) { + r := acctest.TestAccGetResource(resourceName, state) + c, ctx := acctest.TestAccGetPlatformClientWithContext() + id := r.Primary.ID + environment := r.Primary.Attributes["environment"] + orgId := r.Primary.Attributes["org_id"] + projId := r.Primary.Attributes["project_id"] + + target, resp, err := c.TargetsApi.GetTarget((ctx), id, c.AccountId, orgId, projId, environment) + + if err != nil { + return nil, err + } + + if resp == nil { + return nil, nil + } + + return &target, nil +}