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

feat: add endpoint to revert v2 versioning #3897

Merged
merged 7 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions api/environments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
AFTER_DELETE,
AFTER_SAVE,
AFTER_UPDATE,
BEFORE_UPDATE,
LifecycleModel,
hook,
)
Expand All @@ -44,7 +43,6 @@
from environments.exceptions import EnvironmentHeaderNotPresentError
from environments.managers import EnvironmentManager
from features.models import Feature, FeatureSegment, FeatureState
from features.versioning.exceptions import FeatureVersioningError
from metadata.models import Metadata
from projects.models import IdentityOverridesV2MigrationStatus, Project
from segments.models import Segment
Expand Down Expand Up @@ -150,10 +148,6 @@ def clear_environment_cache(self):
# TODO: this could rebuild the cache itself (using an async task)
environment_cache.delete(self.initial_value("api_key"))

@hook(BEFORE_UPDATE, when="use_v2_feature_versioning", was=True, is_now=False)
def validate_use_v2_feature_versioning(self):
raise FeatureVersioningError("Cannot revert from v2 feature versioning.")

@hook(AFTER_DELETE)
def delete_from_dynamo(self):
if self.project.enable_dynamo_db and environment_wrapper.is_enabled:
Expand Down
17 changes: 16 additions & 1 deletion api/environments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
NestedEnvironmentPermissions,
)
from environments.sdk.schemas import SDKEnvironmentDocumentModel
from features.versioning.tasks import enable_v2_versioning
from features.versioning.tasks import (
disable_v2_versioning,
enable_v2_versioning,
)
from permissions.permissions_calculator import get_environment_permission_data
from permissions.serializers import (
PermissionModelSerializer,
Expand Down Expand Up @@ -216,6 +219,18 @@ def enable_v2_versioning(self, request: Request, api_key: str) -> Response:
enable_v2_versioning.delay(kwargs={"environment_id": environment.id})
return Response(status=status.HTTP_202_ACCEPTED)

@swagger_auto_schema(request_body=no_body, responses={202: ""})
@action(detail=True, methods=["POST"], url_path="disable-v2-versioning")
def disable_v2_versioning(self, request: Request, api_key: str) -> Response:
environment = self.get_object()
if environment.use_v2_feature_versioning is False:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": "Environment is not using v2 versioning."},
)
disable_v2_versioning.delay(kwargs={"environment_id": environment.id})
return Response(status=status.HTTP_202_ACCEPTED)


class NestedEnvironmentViewSet(viewsets.GenericViewSet):
model_class = None
Expand Down
29 changes: 28 additions & 1 deletion api/features/versioning/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


@register_task_handler()
def enable_v2_versioning(environment_id: int):
def enable_v2_versioning(environment_id: int) -> None:
from environments.models import Environment

environment = Environment.objects.get(id=environment_id)
Expand All @@ -35,6 +35,33 @@ def enable_v2_versioning(environment_id: int):
environment.save()


@register_task_handler()
def disable_v2_versioning(environment_id: int) -> None:
from environments.models import Environment
from features.models import FeatureState
from features.versioning.models import EnvironmentFeatureVersion

environment = Environment.objects.get(id=environment_id)

latest_feature_states = get_environment_flags_queryset(environment)

# delete any feature states associated with older versions
FeatureState.objects.filter(identity_id__isnull=True).exclude(
id__in=[fs.id for fs in latest_feature_states]
).delete()

# update the latest feature states to be the latest version according
# to the old versioning system
latest_feature_states.update(
version=1, live_from=timezone.now(), environment_feature_version=None
)

EnvironmentFeatureVersion.objects.filter(environment=environment).delete()

environment.use_v2_feature_versioning = False
environment.save()


def _create_initial_feature_versions(environment: "Environment"):
from features.models import Feature, FeatureSegment

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def verify_consistent_responses(num_expected_flags: int) -> None:
# still behave the same
verify_consistent_responses(num_expected_flags=2)

# finally, let's publish the new version and verify that we get a different response
# now, let's publish the new version and verify that we get a different response
publish_ef_version_url = reverse(
"api-v1:versioning:environment-feature-versions-publish",
args=[environment, feature, ef_version_uuid],
Expand Down Expand Up @@ -237,6 +237,45 @@ def verify_consistent_responses(num_expected_flags: int) -> None:
== "v2-segment-override-value"
)

# finally, let's test that we can revert the v2 versioning, and we still get the
# same response
disable_versioning_url = reverse(
"api-v1:environments:environment-disable-v2-versioning",
args=[environment_api_key],
)
environment_update_response = admin_client.post(disable_versioning_url)
assert environment_update_response.status_code == status.HTTP_202_ACCEPTED

time.sleep(0.5)

environment_flags_response_after_revert = get_environment_flags_response_json(
num_expected_flags=2
)
identity_flags_response_after_revert = get_identity_flags_response_json(
num_expected_flags=2
)

# Verify that the environment flags have the same state / value
environment_flag_tuples_pre_revert = {
(f["enabled"], f["feature_state_value"], f["feature"]["id"])
for f in environment_flags_response_json_after_publish
}
environment_flag_tuples_post_revert = {
(f["enabled"], f["feature_state_value"], f["feature"]["id"])
for f in environment_flags_response_after_revert
}
assert environment_flag_tuples_pre_revert == environment_flag_tuples_post_revert

identity_flag_tuples_pre_revert = {
(f["enabled"], f["feature_state_value"], f["feature"]["id"])
for f in identity_flags_response_json_after_publish["flags"]
}
identity_flag_tuples_post_revert = {
(f["enabled"], f["feature_state_value"], f["feature"]["id"])
for f in identity_flags_response_after_revert["flags"]
}
assert identity_flag_tuples_pre_revert == identity_flag_tuples_post_revert


def test_v2_versioning_mv_feature(
admin_client: "APIClient",
Expand Down
Loading