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

FFM-9086: Add a terraform resource to create a new target group #661

Merged
merged 11 commits into from
Sep 1, 2023
3 changes: 3 additions & 0 deletions .changelog/661.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
resource_feature_flag_target_group - Added feature flag target group resources to the Harness Terraform Provider.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "harness_platform_feature_flag_target_group" "target" {
org_id = "test"
project = "test"

identifier = "MY_FEATURE"
environment = "MY_ENVIRONMENT"
name = "MY_FEATURE"
account_id = "MY_ACCOUNT_ID"
included = ["target_id_1"]
excluded = ["target_id_2"]
rules = [
{
attribute = "MY_ATTRIBUTE"
operator = "EQUALS"
value = "MY_VALUE"
}
]
}
2 changes: 2 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/harness/terraform-provider-harness/internal/service/platform/feature_flag"
"github.com/harness/terraform-provider-harness/internal/service/platform/feature_flag_target"
feature_flag_target_group "github.com/harness/terraform-provider-harness/internal/service/platform/feature_flag_target_group"
"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"
Expand Down Expand Up @@ -270,6 +271,7 @@ func Provider(version string) func() *schema.Provider {
"harness_platform_environment_clusters_mapping": pl_environment_clusters_mapping.ResourceEnvironmentClustersMapping(),
"harness_platform_environment_service_overrides": pl_environment_service_overrides.ResourceEnvironmentServiceOverrides(),
"harness_platform_feature_flag": feature_flag.ResourceFeatureFlag(),
"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_ff_api_key": ff_api_key.ResourceFFApiKey(),
Expand Down
3 changes: 2 additions & 1 deletion internal/service/cd/delegate/delegate_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/harness/harness-go-sdk/harness/cd/graphql"
"github.com/harness/harness-go-sdk/harness/delegate"
Expand Down Expand Up @@ -63,7 +64,7 @@ func deleteDelegate(t *testing.T, name string) {
cli, err := client.NewClientWithOpts(client.FromEnv)
require.NoError(t, err, "failed to create docker client: %s", err)

err = cli.ContainerStop(context.Background(), name, nil)
err = cli.ContainerStop(context.Background(), name, container.StopOptions{})
ribeirophillipe marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err, "failed to stop delegate container: %s", err)

err = cli.ContainerRemove(context.Background(), name, types.ContainerRemoveOptions{})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
package featureflagtargetgroup

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"
)

// ResourceFeatureFlagTargetGroup ...
func ResourceFeatureFlagTargetGroup() *schema.Resource {
resource := &schema.Resource{
Description: "Resource for creating a Harness Feature Flag Target Group.",

ReadContext: resourceFeatureFlagTargetGroupRead,
CreateContext: resourceFeatureFlagTargetCreate,
UpdateContext: resourceFeatureFlagTargetGroupUpdate,
DeleteContext: resourceFeatureFlagTargetGroupDelete,
Importer: helpers.ProjectResourceImporter,

Schema: map[string]*schema.Schema{
"identifier": {
Description: "The unique identifier of the feature flag target group.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"org_id": {
Description: "Organization Identifier",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": {
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: "The name of the feature flag target group.",
Type: schema.TypeString,
Required: true,
},
"included": {
ribeirophillipe marked this conversation as resolved.
Show resolved Hide resolved
Description: "A list of targets to include in the target group",
Type: schema.TypeList,
Optional: true,
MinItems: 0,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"excluded": {
Description: "A list of targets to exclude from the target group",
Type: schema.TypeList,
Optional: true,
MinItems: 0,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"rules": {
Description: "The list of rules to include in the feature flag target group.",
ribeirophillipe marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"attribute": {
Description: "The attribute to use in the clause. This can be any target attribute",
Type: schema.TypeString,
Optional: true,
},
"negate": {
Description: "Is the operation negated?",
Type: schema.TypeBool,
Optional: true,
},
"op": {
Description: "The type of operation such as equals, starts_with, contains",
Type: schema.TypeString,
Optional: true,
},
"values": {
Description: "The values that are compared against the operator",
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
},
},
}

return resource
}

// FFTargetGroupQueryParameters ...
type FFTargetGroupQueryParameters struct {
Identifier string `json:"identifier,omitempty"`
OrgID string `json:"orgId,omitempty"`
Project string `json:"project,omitempty"`
AcountID string `json:"accountId,omitempty"`
Environment string `json:"environment,omitempty"`
}

// FFTargetGroupOpts ...
type FFTargetGroupOpts struct {
Identifier string `json:"identifier,omitempty"`
Name string `json:"name,omitempty"`
Included []nextgen.Target `json:"included,omitempty"`
Excluded []nextgen.Target `json:"excluded,omitempty"`
Rules []nextgen.Clause `json:"rules,omitempty"`
}

// SegmentRequest ...
type SegmentRequest struct {
Identifier string `json:"identifier,omitempty"`
Project string `json:"project,omitempty"`
Environment string `json:"environment,omitempty"`
Name string `json:"name,omitempty"`
Included []string `json:"included,omitempty"`
Excluded []string `json:"excluded,omitempty"`
Rules []nextgen.Clause `json:"rules,omitempty"`
}

func resourceFeatureFlagTargetGroupRead(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 := buildFFTargetGroupQueryParameters(d)

segment, httpResp, err := c.TargetGroupsApi.GetSegment(ctx, c.AccountId, qp.OrgID, id, qp.Project, qp.Environment)
if err != nil {
return helpers.HandleReadApiError(err, d, httpResp)
}

readFeatureFlagTargetGroup(d, &segment, qp)

return nil
}

// resourceFeatureFlagTargetGroupCreate ...
func resourceFeatureFlagTargetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c, ctx := meta.(*internal.Session).GetPlatformClientWithContext(ctx)
var err error
var httpResp *http.Response
var segment nextgen.Segment
segmentRequest := buildSegmentRequest(d)
qp := buildFFTargetGroupQueryParameters(d)
id := d.Id()
if id == "" {
id = d.Get("identifier").(string)
d.MarkNewResource()
}

httpResp, err = c.TargetGroupsApi.CreateSegment(ctx, segmentRequest, c.AccountId, qp.OrgID)

if err != nil {
return helpers.HandleApiError(err, d, httpResp)
}

if httpResp.StatusCode != http.StatusCreated {
return diag.Errorf("createstatus: %s", httpResp.Status)
}

segment, httpResp, err = c.TargetGroupsApi.GetSegment(ctx, c.AccountId, qp.OrgID, id, qp.Project, qp.Environment)
if err != nil {
body, _ := io.ReadAll(httpResp.Body)
return diag.Errorf("readstatus: %s, \nBody:%s", httpResp.Status, body)
}

readFeatureFlagTargetGroup(d, &segment, qp)

return nil
}

func resourceFeatureFlagTargetGroupUpdate(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 := buildFFTargetGroupQueryParameters(d)
opts := buildFFTargetGroupOpts(d)

var err error
var segment nextgen.Segment
var httpResp *http.Response

segment, httpResp, err = c.TargetGroupsApi.PatchSegment(ctx, c.AccountId, qp.OrgID, qp.Project, qp.Environment, id, opts)
if err != nil {
return helpers.HandleApiError(err, d, httpResp)
}

time.Sleep(1 * time.Second)

segment, httpResp, err = c.TargetGroupsApi.GetSegment(ctx, c.AccountId, qp.OrgID, id, qp.Project, qp.Environment)
if err != nil {
body, _ := io.ReadAll(httpResp.Body)
return diag.Errorf("readstatus: %s, \nBody:%s", httpResp.Status, body)
}

readFeatureFlagTargetGroup(d, &segment, qp)

return nil
}

func resourceFeatureFlagTargetGroupDelete(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 := buildFFTargetGroupQueryParameters(d)

httpResp, err := c.TargetGroupsApi.DeleteSegment(ctx, c.AccountId, qp.OrgID, id, qp.Project, qp.Environment)
if err != nil {
return helpers.HandleApiError(err, d, httpResp)
}

return nil
}

// readFeatureFlagTargetGroupRule ...
func readFeatureFlagTargetGroup(d *schema.ResourceData, segment *nextgen.Segment, qp *FFTargetGroupQueryParameters) {
d.SetId(segment.Identifier)
d.Set("identifier", segment.Identifier)
d.Set("org_id", qp.OrgID)
d.Set("project", qp.Project)
d.Set("account_id", qp.AcountID)
d.Set("environment", segment.Environment)
d.Set("name", segment.Name)
d.Set("included", segment.Included)
d.Set("excluded", segment.Excluded)
d.Set("rules", segment.Rules)
}

// buildFFTargetGroupQueryParameters ...
func buildFFTargetGroupQueryParameters(d *schema.ResourceData) *FFTargetGroupQueryParameters {
return &FFTargetGroupQueryParameters{
Identifier: d.Get("identifier").(string),
OrgID: d.Get("org_id").(string),
Project: d.Get("project").(string),
AcountID: d.Get("account_id").(string),
Environment: d.Get("environment").(string),
}
}

// buildSegmentRequest builds a SegmentRequest from a ResourceData
func buildSegmentRequest(d *schema.ResourceData) *SegmentRequest {
opts := &SegmentRequest{
Identifier: d.Get("identifier").(string),
Project: d.Get("project").(string),
Environment: d.Get("environment").(string),
Name: d.Get("name").(string),
}

if included, ok := d.GetOk("included"); ok {
opts.Included = included.([]string)
}

if excluded, ok := d.GetOk("excluded"); ok {
opts.Excluded = excluded.([]string)
}

if rules, ok := d.GetOk("rules"); ok {
opts.Rules = rules.([]nextgen.Clause)
}

return opts
}

// buildFFTargetGroupOpts ...
func buildFFTargetGroupOpts(d *schema.ResourceData) *nextgen.TargetGroupsApiPatchSegmentOpts {
opts := &FFTargetGroupOpts{
Identifier: d.Get("identifier").(string),
Name: d.Get("name").(string),
}

if included, ok := d.GetOk("included"); ok {
opts.Included = included.([]nextgen.Target)
}

if excluded, ok := d.GetOk("excluded"); ok {
opts.Excluded = excluded.([]nextgen.Target)
}

if rules, ok := d.GetOk("rules"); ok {
opts.Rules = rules.([]nextgen.Clause)
}

return &nextgen.TargetGroupsApiPatchSegmentOpts{
Body: optional.NewInterface(opts),
}
}
Loading