def _toggle_flag(module, patches, feature_flag): if module.params["state"] == "enabled": value = True elif module.params["state"] == "disabled": value = False else: value = feature_flag.on if feature_flag.on != value: path = _patch_path(module, "on") patches.append( launchdarkly_api.PatchOperation(path=path, op="replace", value=value) ) return patches
def _toggle_flag(state, patches, feature_flag, env): if state == "enabled": value = True elif state == "disabled": value = False else: value = feature_flag.on if feature_flag.on != value: path = _patch_path(env, "on") patches.append( launchdarkly_api.PatchOperation(path=path, op="replace", value=value) ) return patches
def _process_rules(module, patches, feature_flag): old_rules = max(len(feature_flag.rules) - 1, 0) new_index = len(module.params["rules"]) - 1 # Make copy for next step. new_rules_copy = copy.deepcopy(module.params["rules"]) flag_index = 0 for new_rule_index, rule in enumerate(module.params["rules"]): state = rule.get("rule_state", "present") if rule.get("rule_state"): del rule["rule_state"] # Trim old rules, if state isn't add if new_index < old_rules and state != "add": path = _patch_path(module, "rules") + "/" + str(new_index) # LD Patch requires value, so passing in dictionary patches.append(dict(op="remove", path=path)) if new_rule_index <= old_rules and state != "add": # iterating over statements for range to be inclusive if new_rule_index <= len(feature_flag.rules) - 1: # API returns None for rollout and variation if not set if not rule.get("rollout"): rule["rollout"] = None else: # If there's a role and no bucket_by, default to key rule["rollout"]["bucket_by"] = rule["rollout"].get( "bucket_by", "key" ) # Weighted variations is internal ansible/API name, map to public rule["rollout"]["variations"] = rule["rollout"].pop( "weighted_variations" ) if rule.get("variation") is None: rule["variation"] = None for clause in rule["clauses"]: if clause.get("negate") is None: clause["negate"] = False flag = feature_flag.rules[new_rule_index].to_dict() if list( diff(rule, flag, ignore=set(["id", "rule_state", "track_events"])) ): path = _patch_path(module, "rules") try: if rule["variation"] is not None: patches.append( _patch_op( "replace", path + "/%d/variation" % new_rule_index, rule["variation"], ) ) except KeyError: pass if rule["rollout"]: if flag.get("variation") is not None: patches.append( dict( op="remove", path=path + "/%d/variation" % new_rule_index, ) ) op = "add" else: op = "replace" patches.append( _patch_op( op, path + "/%d/rollout" % new_rule_index, _build_rules(rule["rollout"]), ) ) if rule["clauses"] is not None: for clause_idx, clause in enumerate(rule["clauses"]): patches.append( _patch_op( "replace", path + "/%d/clauses/%d/op" % (new_rule_index, clause_idx), clause["op"], ) ) try: patches.append( _patch_op( "replace", path + "/%d/clauses/%d/negate" % (new_rule_index, clause_idx), clause["negate"], ) ) except KeyError: # pass patches.append( _patch_op( "replace", path + "/%d/clauses/%d/negate" % (new_rule_index, clause_idx), False, ) ) patches.append( _patch_op( "replace", path + "/%d/clauses/%d/values" % (new_rule_index, clause_idx), clause["values"], ) ) patches.append( _patch_op( "replace", path + "/%d/clauses/%d/attribute" % (new_rule_index, clause_idx), clause["attribute"], ) ) if new_rules_copy: new_rules_copy.pop(0) flag_index += 1 result = [] for idx, rule_change in enumerate(new_rules_copy): rule = _build_rules(rule_change) new_flag_index = flag_index + idx rule_change["state"] = rule_change.get("state", "present") if idx > old_rules: path = _patch_path(module, "rules") + "/" + str(idx) patches.append(_patch_op("add", path, rule)) # Non-idempotent operation - add elif rule_change["state"] == "add": pos = old_rules + idx path = _patch_path(module, "rules") + "/" + str(pos) patches.append(_patch_op("add", path, rule)) else: state = rule_change["state"] del rule_change["state"] # Needed because nested defaults are not applying for clause in rule_change["clauses"]: clause["negate"] = clause.get("negate", False) if idx < old_rules: result = list( diff( rule_change, feature_flag.rules[idx].to_dict(), ignore=set(["id"]), ) ) if result: path = _patch_path(module, "rules") + "/" + str(new_flag_index) patches.append(_patch_op("replace", path, rule)) elif not result and state == "absent": path = _patch_path(module, "rules") + "/" + str(new_flag_index) patches.append(_patch_op("remove", path, rule)) else: # If a previous rule exists increment index to add new rule if len(feature_flag.rules) > 0 and old_rules == 0: old_rules = 1 pos = old_rules + idx path = _patch_path(module, "rules") + "/" + str(pos) patches.append(_patch_op("add", path, rule)) del module.params["rules"]
def _configure_feature_flag_env(module, api_instance, feature_flag=None): if module.params["conftest"]["enabled"]: rego_test(module) patches = [] _toggle_flag(module, patches, feature_flag) if ( feature_flag.off_variation == module.params["off_variation"] or module.params.get("off_variation") is None ): del module.params["off_variation"] if ( feature_flag.track_events == module.params["track_events"] or module.params.get("track_events") is None ): del module.params["track_events"] # Loop over prerequisites comparing _check_prereqs(module, feature_flag) # Loop over targets comparing if module.params["targets"] is not None: flag_var_index = {} # Map variation to index flag targets first: for idx, target in enumerate(feature_flag.targets): target_dict = target.to_dict() target_index = str(target_dict["variation"]) wtf = str(idx) flag_var_index = { target_dict["variation"]: { "index": wtf, "targets": target_dict["values"], } } # Check if targets already exist in variation for target in module.params["targets"]: if target["state"] == "add": if flag_var_index: if set(target["values"]).issubset( set(flag_var_index[target["variation"]]["targets"]) ): continue else: new_targets = list( set(target["values"]) - set(flag_var_index[target["variation"]]["targets"]) ) target_index = str(flag_var_index[target["variation"]]["index"]) new_targets_idx = len( flag_var_index[target["variation"]]["targets"] ) for val_idx, val in enumerate(new_targets): new_idx = str(new_targets_idx + val_idx) path = ( _patch_path(module, "targets") + "/" + target_index + "/values/" + new_idx ) patches.append(_patch_op("add", path, new_targets[val_idx])) continue else: new_targets = set(target["values"]) target_index = "0" val_index = "0" elif target["state"] == "replace": if flag_var_index: if set(target["values"]) == set( flag_var_index[target["variation"]]["targets"] ): continue else: new_targets = set(target["values"]) target_index = str(flag_var_index[target["variation"]]["index"]) val_index = str(flag_var_index[target["variation"]]["index"]) else: new_targets = set(target["values"]) target_index = "0" # Replace does not work on empty targets target["state"] = "add" elif target["state"] == "remove": if set(target["values"]).issubset( set(flag_var_index[target["variation"]]["targets"]) ): new_targets = set(target["values"]) target_index = str(flag_var_index[target["variation"]]["index"]) val_index = str(flag_var_index[target["variation"]]["index"]) else: raise AnsibleError("Targets not found") elif target["state"] == "absent": try: target_index = str(flag_var_index[target["variation"]]["index"]) path = _patch_path(module, "targets") + "/" + target_index patches.append(dict(op="remove", path=path)) except KeyError: pass continue path = _patch_path(module, "targets") + "/" + target_index patches.append( _patch_op( target["state"], path, {"variation": target["variation"], "values": list(new_targets)}, ) ) del module.params["targets"] # Loop over rules comparing if module.params["rules"] is not None: _process_rules(module, patches, feature_flag) # Compare fallthrough fallthrough = diff( module.params["fallthrough"], feature_flag.fallthrough.to_dict(), ignore=set(["id"]), ) if not list(fallthrough): del module.params["fallthrough"] else: fallthrough = _build_rules(module.params["fallthrough"]) op = "replace" path = _patch_path(module, "fallthrough") patches.append(_patch_op(op, path, fallthrough)) # Delete key so it's not passed through to next loop del module.params["fallthrough"] for key in module.params: if ( key not in [ "state", "api_key", "environment_key", "project_key", "flag_key", "comment", "salt", "conftest", ] and module.params[key] is not None ): patches.append(_parse_flag_param(module, key)) if patches: comments = dict(comment=_build_comment(module), patch=patches) try: api_response = api_instance.patch_feature_flag( module.params["project_key"], module.params["flag_key"], patch_comment=comments, ) except Exception as e: raise AnsibleError("Error applying configuration: %s" % to_native(e)) output_patches = [] for patch in patches: if type(patch) is dict: output_patches.append(patch) else: output_patches.append(patch.to_dict()) module.exit_json( changed=True, msg="flag environment successfully configured", feature_flag_environment=api_response.to_dict(), patches=output_patches, ) module.exit_json( changed=False, msg="flag environment unchanged", feature_flag_environment=feature_flag.to_dict(), )
def _parse_flag_param(module, key, op="replace"): path = _patch_path(module, launchdarkly_api.FeatureFlagConfig.attribute_map[key]) return launchdarkly_api.PatchOperation(path=path, op=op, value=module.params[key])
def configure_feature_flag_env(params, feature_flag): env = params["environment_key"] patches = [] clauses_list = [] _toggle_flag(params["state"], patches, feature_flag, env) if ( feature_flag.off_variation == params["off_variation"] or params.get("off_variation") is None ): del params["off_variation"] if ( feature_flag.track_events == params["track_events"] or params.get("track_events") is None ): del params["track_events"] # Loop over prerequisites comparing _check_prereqs(params["prerequisites"], feature_flag) # Loop over targets comparing if params["targets"] is not None: flag_var_index = {} # Map variation to index flag targets first: for idx, target in enumerate(feature_flag.targets): target_dict = target.to_dict() target_index = str(target_dict["variation"]) wtf = str(idx) flag_var_index = { target_dict["variation"]: { "index": wtf, "targets": target_dict["values"], } } # Check if targets already exist in variation for target in params["targets"]: if target["state"] == "add": if flag_var_index: if set(target["values"]).issubset( set(flag_var_index[target["variation"]]["targets"]) ): continue else: new_targets = list( set(target["values"]) - set(flag_var_index[target["variation"]]["targets"]) ) target_index = str(flag_var_index[target["variation"]]["index"]) new_targets_idx = len( flag_var_index[target["variation"]]["targets"] ) for val_idx, val in enumerate(new_targets): new_idx = str(new_targets_idx + val_idx) path = ( _patch_path(env, "targets") + "/" + target_index + "/values/" + new_idx ) patches.append(_patch_op("add", path, new_targets[val_idx])) continue else: new_targets = set(target["values"]) target_index = "0" val_index = "0" elif target["state"] == "replace": if flag_var_index: if set(target["values"]) == set( flag_var_index[target["variation"]]["targets"] ): continue else: new_targets = set(target["values"]) target_index = str(flag_var_index[target["variation"]]["index"]) val_index = str(flag_var_index[target["variation"]]["index"]) else: new_targets = set(target["values"]) target_index = "0" # Replace does not work on empty targets target["state"] = "add" elif target["state"] == "remove": if set(target["values"]).issubset( set(flag_var_index[target["variation"]]["targets"]) ): new_targets = set(target["values"]) target_index = str(flag_var_index[target["variation"]]["index"]) val_index = str(flag_var_index[target["variation"]]["index"]) else: raise AnsibleError("Targets not found") elif target["state"] == "absent": try: target_index = str(flag_var_index[target["variation"]]["index"]) path = _patch_path(env, "targets") + "/" + target_index patches.append(dict(op="remove", path=path)) except KeyError: pass continue path = _patch_path(env, "targets") + "/" + target_index patches.append( _patch_op( target["state"], path, {"variation": target["variation"], "values": list(new_targets)}, ) ) del params["targets"] # Loop over rules comparing if params["rules"] is not None: rule_patches, rule_clauses = _process_rules(params["rules"], feature_flag, env) del params["rules"] patches.extend(rule_patches) clauses_list.extend(rule_clauses) # Compare fallthrough fallthrough = diff( params["fallthrough"], feature_flag.fallthrough.to_dict(), ignore=set(["id"]), ) if not list(fallthrough): del params["fallthrough"] else: fallthrough = _build_rules(params["fallthrough"]) op = "replace" path = _patch_path(env, "fallthrough") patches.append(_patch_op(op, path, fallthrough)) # Delete key so it's not passed through to next loop del params["fallthrough"] for key in params: if ( key not in [ "state", "api_key", "environment_key", "project_key", "flag_key", "comment", "salt", "conftest", ] and params[key] is not None ): patches.append(_parse_flag_param(params, env, key)) return patches, clauses_list