Exemple #1
0
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
Exemple #2
0
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
Exemple #3
0
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"]
Exemple #4
0
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(),
    )
Exemple #5
0
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])
Exemple #6
0
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