Skip to content

Commit

Permalink
feat: add context_kind to targeting rules with percentage rollouts (#293
Browse files Browse the repository at this point in the history
)
  • Loading branch information
ldhenry authored Feb 11, 2025
1 parent c908f7e commit a41f969
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/data-sources/feature_flag_environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Read-Only:

- `bucket_by` (String)
- `clauses` (List of Object) (see [below for nested schema](#nestedobjatt--rules--clauses))
- `context_kind` (String)
- `description` (String)
- `rollout_weights` (List of Number)
- `variation` (Number)
Expand Down
5 changes: 4 additions & 1 deletion docs/resources/feature_flag_environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ resource "launchdarkly_feature_flag_environment" "big_flag_environment" {
op = "segmentMatch" // Maps to 'Context is in' in the UI
values = ["test-segment"]
}
variation = 0
rollout_weights = [40000, 60000]
bucket_by = "country"
context_kind = "account"
}
fallthrough {
Expand Down Expand Up @@ -240,6 +242,7 @@ Optional:

- `bucket_by` (String) Group percentage rollout by a custom attribute. This argument is only valid if `rollout_weights` is also specified.
- `clauses` (Block List) List of nested blocks specifying the logical clauses to evaluate (see [below for nested schema](#nestedblock--rules--clauses))
- `context_kind` (String) The context kind associated with the specified rollout. This argument is only valid if `rollout_weights` is also specified. Defaults to `user` if omitted.
- `description` (String) A human-readable description of the targeting rule.
- `rollout_weights` (List of Number) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000 and the number of rollout weights specified in the array must match the number of flag variations. You must specify either `variation` or `rollout_weights`.
- `variation` (Number) The integer variation index to serve if the rule clauses evaluate to `true`. You must specify either `variation` or `rollout_weights`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ resource "launchdarkly_feature_flag_environment" "big_flag_environment" {
op = "segmentMatch" // Maps to 'Context is in' in the UI
values = ["test-segment"]
}
variation = 0
rollout_weights = [40000, 60000]
bucket_by = "country"
context_kind = "account"
}

fallthrough {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) {
},
},
},
{
Description: strPtr("test rule with rollout"),
Clauses: []ldapi.Clause{
{
Attribute: "thing",
Op: "contains",
Values: []interface{}{"test-2"},
},
},
Rollout: &ldapi.Rollout{
Variations: []ldapi.WeightedVariation{
{Variation: int32(0), Weight: int32(20000)},
{Variation: int32(1), Weight: int32(80000)},
},
BucketBy: strPtr("country"),
ContextKind: strPtr("account"),
},
},
}
prerequisites := []ldapi.Prerequisite{
{
Expand Down Expand Up @@ -161,6 +179,10 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.attribute", thisConfig.Rules[0].Clauses[0].Attribute),
resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.op", thisConfig.Rules[0].Clauses[0].Op),
resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", fmt.Sprint(thisConfig.Rules[0].Clauses[0].Values[0])),
resource.TestCheckResourceAttr(resourceName, "rules.1.description", *thisConfig.Rules[1].Description),
resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.#", "2"),
resource.TestCheckResourceAttr(resourceName, "rules.1.bucket_by", *thisConfig.Rules[1].Rollout.BucketBy),
resource.TestCheckResourceAttr(resourceName, "rules.1.context_kind", *thisConfig.Rules[1].Rollout.ContextKind),
resource.TestCheckResourceAttr(resourceName, "prerequisites.0.flag_key", thisConfig.Prerequisites[0].Key),
resource.TestCheckResourceAttr(resourceName, "prerequisites.0.variation", fmt.Sprint(thisConfig.Prerequisites[0].Variation)),
resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, fmt.Sprint(*thisConfig.OffVariation)),
Expand Down
2 changes: 1 addition & 1 deletion launchdarkly/fallthrough_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func fallthroughFromResourceData(d *schema.ResourceData) (fallthroughModel, erro

fall := f[0].(map[string]interface{})
if isPercentRollout(f) {
rollout := fallthroughModel{Rollout: rolloutFromResourceData(fall[ROLLOUT_WEIGHTS])}
rollout := fallthroughModel{Rollout: rolloutFromResourceData(fall[ROLLOUT_WEIGHTS].([]interface{}))}
bucketBy := fall[BUCKET_BY].(string)
if bucketBy != "" {
rollout.Rollout.BucketBy = &bucketBy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ resource "launchdarkly_feature_flag_environment" "basic" {
}
rollout_weights = [90000, 10000, 0]
bucket_by = "email"
context_kind = "account"
}
fallthrough {
Expand Down Expand Up @@ -737,6 +738,7 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.1", "10000"),
resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.2", "0"),
resource.TestCheckResourceAttr(resourceName, "rules.1.bucket_by", "email"),
resource.TestCheckResourceAttr(resourceName, "rules.1.context_kind", "account"),
resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.attribute", "name"),
resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.op", "startsWith"),
resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.#", "1"),
Expand Down
13 changes: 6 additions & 7 deletions launchdarkly/rollout_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ func rolloutSchema() *schema.Schema {
}
}

func rolloutFromResourceData(val interface{}) *ldapi.Rollout {
rolloutList := val.([]interface{})
variations := []ldapi.WeightedVariation{}
for idx, k := range rolloutList {
func rolloutFromResourceData(rolloutWeights []interface{}) *ldapi.Rollout {
variations := make([]ldapi.WeightedVariation, 0, len(rolloutWeights))
for idx, k := range rolloutWeights {
weight := k.(int)
variations = append(variations,
ldapi.WeightedVariation{
Expand All @@ -43,11 +42,11 @@ func rolloutFromResourceData(val interface{}) *ldapi.Rollout {
return &r
}

func rolloutsToResourceData(rollouts *ldapi.Rollout) interface{} {
transformed := make([]interface{}, len(rollouts.Variations))
func rolloutsToResourceData(rollouts *ldapi.Rollout) []interface{} {
transformed := make([]interface{}, 0, len(rollouts.Variations))

for _, r := range rollouts.Variations {
transformed[r.Variation] = r.Weight
transformed = append(transformed, r.Weight)
}
return transformed
}
51 changes: 44 additions & 7 deletions launchdarkly/rule_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ func rulesSchema(isDataSource bool) *schema.Schema {
Optional: true,
Description: "Group percentage rollout by a custom attribute. This argument is only valid if `rollout_weights` is also specified.",
},
CONTEXT_KIND: {
Type: schema.TypeString,
Optional: true,
Description: "The context kind associated with the specified rollout. This argument is only valid if `rollout_weights` is also specified. Defaults to `user` if omitted.",
DiffSuppressFunc: ruleContextKindDiffSuppressFunc(),
},
},
},
}
Expand All @@ -62,8 +68,28 @@ func rulesFromResourceData(d *schema.ResourceData) ([]rule, error) {
return rules, nil
}

func ruleHasPercentageRollout(ruleMap map[string]interface{}) bool {
return len(ruleMap[ROLLOUT_WEIGHTS].([]interface{})) > 0
}

func validateRuleResourceData(ruleMap map[string]interface{}) error {
if !ruleHasPercentageRollout(ruleMap) {
if ruleMap[BUCKET_BY].(string) != "" {
return errors.New("rules: cannot use bucket_by argument with variation, only with rollout_weights")
}
if ruleMap[CONTEXT_KIND].(string) != "" {
return errors.New("rules: cannot use context_kind argument with variation, only with rollout_weights")
}
}
return nil
}

func ruleFromResourceData(val interface{}) (rule, error) {
ruleMap := val.(map[string]interface{})
err := validateRuleResourceData(ruleMap)
if err != nil {
return rule{}, err
}
var r rule
for _, c := range ruleMap[CLAUSES].([]interface{}) {
clause, err := clauseFromResourceData(c)
Expand All @@ -73,16 +99,17 @@ func ruleFromResourceData(val interface{}) (rule, error) {
r.Clauses = append(r.Clauses, clause)
}
bucketBy := ruleMap[BUCKET_BY].(string)
bucketByFound := bucketBy != ""
if len(rolloutFromResourceData(ruleMap[ROLLOUT_WEIGHTS]).Variations) > 0 {
r.Rollout = rolloutFromResourceData(ruleMap[ROLLOUT_WEIGHTS])
if bucketByFound {
contextKind := ruleMap[CONTEXT_KIND].(string)
rollout := rolloutFromResourceData(ruleMap[ROLLOUT_WEIGHTS].([]interface{}))
if len(rollout.Variations) > 0 {
r.Rollout = rollout
if bucketBy != "" {
r.Rollout.BucketBy = &bucketBy
}
} else {
if bucketByFound && bucketBy != "" {
return r, errors.New("rules: cannot use bucket_by argument with variation, only with rollout_weights")
if contextKind != "" {
r.Rollout.ContextKind = &contextKind
}
} else {
r.Variation = intPtr(ruleMap[VARIATION].(int))
}
description := ruleMap[DESCRIPTION].(string)
Expand All @@ -106,6 +133,7 @@ func rulesToResourceData(rules []ldapi.Rule) (interface{}, error) {
if r.Rollout != nil {
ruleMap[ROLLOUT_WEIGHTS] = rolloutsToResourceData(r.Rollout)
ruleMap[BUCKET_BY] = r.Rollout.BucketBy
ruleMap[CONTEXT_KIND] = r.Rollout.ContextKind
} else {
ruleMap[VARIATION] = r.Variation
}
Expand All @@ -116,3 +144,12 @@ func rulesToResourceData(rules []ldapi.Rule) (interface{}, error) {
}
return transformed, nil
}

func ruleContextKindDiffSuppressFunc() schema.SchemaDiffSuppressFunc {
return func(k, oldValue, newValue string, d *schema.ResourceData) bool {
if oldValue == "user" && newValue == "" {
return true
}
return false
}
}
Loading

0 comments on commit a41f969

Please sign in to comment.