Skip to content

Commit

Permalink
fix(versioning): fix cloning environments using v2 versioning (#3999)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewelwell authored May 21, 2024
1 parent 16d1dda commit eef02fb
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 5 deletions.
28 changes: 26 additions & 2 deletions api/environments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from environments.managers import EnvironmentManager
from features.models import Feature, FeatureSegment, FeatureState
from features.multivariate.models import MultivariateFeatureStateValue
from features.versioning.models import EnvironmentFeatureVersion
from metadata.models import Metadata
from projects.models import Project
from segments.models import Segment
Expand Down Expand Up @@ -174,8 +175,31 @@ def clone(self, name: str, api_key: str = None) -> "Environment":
# Since identities are closely tied to the environment
# it does not make much sense to clone them, hence
# only clone feature states without identities
for feature_state in self.feature_states.filter(identity=None):
feature_state.clone(clone, live_from=feature_state.live_from)
queryset = self.feature_states.filter(identity=None)

if self.use_v2_feature_versioning:
# Grab the latest feature versions from the source environment.
latest_environment_feature_versions = (
EnvironmentFeatureVersion.objects.get_latest_versions_as_queryset(
environment=self
)
)

# Create a dictionary holding the environment feature versions (unique per feature)
# to use in the cloned environment.
clone_environment_feature_versions = {
efv.feature_id: efv.clone_to_environment(environment=clone)
for efv in latest_environment_feature_versions
}

for feature_state in queryset.filter(
environment_feature_version__in=latest_environment_feature_versions
):
clone_efv = clone_environment_feature_versions[feature_state.feature_id]
feature_state.clone(clone, environment_feature_version=clone_efv)
else:
for feature_state in queryset:
feature_state.clone(clone, live_from=feature_state.live_from)

return clone

Expand Down
20 changes: 17 additions & 3 deletions api/features/versioning/managers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import typing
from pathlib import Path

from django.db.models.query import RawQuerySet
from django.db.models.query import QuerySet, RawQuerySet
from softdelete.models import SoftDeleteManager

if typing.TYPE_CHECKING:
from environments.models import Environment
from features.versioning.models import EnvironmentFeatureVersion


with open(Path(__file__).parent.resolve() / "sql/get_latest_versions.sql") as f:
Expand All @@ -15,9 +16,22 @@
class EnvironmentFeatureVersionManager(SoftDeleteManager):
def get_latest_versions(self, environment: "Environment") -> RawQuerySet:
"""
Get the latest EnvironmentFeatureVersion objects
for a given environment.
Get the latest EnvironmentFeatureVersion objects for a given environment.
"""
return self.raw(
get_latest_versions_sql, params={"environment_id": environment.id}
)

def get_latest_versions_as_queryset(
self, environment: "Environment"
) -> QuerySet["EnvironmentFeatureVersion"]:
"""
Get the latest EnvironmentFeatureVersion objects for a given environment
as a concrete QuerySet.
Note that it is often required to return the proper QuerySet to carry out
operations on the ORM object.
"""
return self.filter(
uuid__in=[efv.uuid for efv in self.get_latest_versions(environment)]
)
12 changes: 12 additions & 0 deletions api/features/versioning/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import typing
import uuid
from copy import deepcopy

from core.models import (
SoftDeleteExportableModel,
Expand Down Expand Up @@ -128,3 +129,14 @@ def publish(
if persist:
self.save()
environment_feature_version_published.send(self.__class__, instance=self)

def clone_to_environment(
self, environment: "Environment"
) -> "EnvironmentFeatureVersion":
_clone = deepcopy(self)

_clone.uuid = None
_clone.environment = environment

_clone.save()
return _clone
57 changes: 57 additions & 0 deletions api/tests/unit/environments/test_unit_environments_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
from features.models import Feature, FeatureState
from features.multivariate.models import MultivariateFeatureOption
from features.versioning.models import EnvironmentFeatureVersion
from features.versioning.tasks import enable_v2_versioning
from features.versioning.versioning_service import (
get_environment_flags_queryset,
)
from organisations.models import Organisation, OrganisationRole
from projects.models import EdgeV2MigrationStatus, Project
from segments.models import Segment
Expand Down Expand Up @@ -937,3 +941,56 @@ def test_create_environment_creates_feature_states_in_all_environments_and_envir
EnvironmentFeatureVersion.objects.filter(environment=environment).count() == 2
)
assert environment.feature_states.count() == 2


def test_clone_environment_v2_versioning(
feature: Feature,
feature_state: FeatureState,
segment: Segment,
segment_featurestate: FeatureState,
environment: Environment,
) -> None:
# Given
expected_environment_fs_enabled_value = True
expected_segment_fs_enabled_value = True

# First let's create some new versions via the old versioning methods
feature_state.clone(environment, version=2)
feature_state.clone(environment, version=3)

# and a draft version
feature_state.clone(environment, as_draft=True)

# Now let's enable v2 versioning for the environment
enable_v2_versioning(environment.id)
environment.refresh_from_db()

# Finally, let's create another version using the new versioning methods
# and update some values on the feature states in it.
v2 = EnvironmentFeatureVersion.objects.create(
feature=feature, environment=environment
)
v2.feature_states.filter(feature_segment__isnull=True).update(
enabled=expected_environment_fs_enabled_value
)
v2.feature_states.filter(feature_segment__isnull=False).update(
enabled=expected_segment_fs_enabled_value
)
v2.publish()

# When
cloned_environment = environment.clone(name="Cloned environment")

# Then
assert cloned_environment.use_v2_feature_versioning is True

cloned_environment_flags = get_environment_flags_queryset(cloned_environment)

assert (
cloned_environment_flags.get(feature_segment__isnull=True).enabled
is expected_environment_fs_enabled_value
)
assert (
cloned_environment_flags.get(feature_segment__segment=segment).enabled
is expected_segment_fs_enabled_value
)

0 comments on commit eef02fb

Please sign in to comment.