Skip to content

Commit dfb2ec7

Browse files
committed
Fix post-migrate call. Replace with manual migrations
1 parent 2ef72a1 commit dfb2ec7

File tree

11 files changed

+128
-19
lines changed

11 files changed

+128
-19
lines changed

ansible_base/feature_flags/apps.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,4 @@ class FeatureFlagsConfig(AppConfig):
1111
verbose_name = 'Feature Flags'
1212

1313
def ready(self):
14-
try:
15-
create_initial_data()
16-
except Exception:
17-
post_migrate.connect(create_initial_data, sender=self)
14+
post_migrate.connect(create_initial_data, sender=self)

ansible_base/feature_flags/definitions/feature_flags.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
condition: boolean
55
value: 'False'
66
support_level: TECHNICAL_PREVIEW
7-
description: TBD
8-
support_url: https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/
7+
description: "Indirect Node Counting parses the event stream of all jobs to identify resources and stores these in the platform database. Example: Job automates VMware, the parser will report back the VMs, Hypervisors that were automated. This feature helps customers and partners report on the automations they are doing beyond an API endpoint."
8+
support_url: https://access.redhat.com/articles/7109910
99
labels:
1010
- controller
1111
- name: FEATURE_EDA_ANALYTICS_ENABLED

ansible_base/feature_flags/migrations/0001_initial.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Generated by Django 4.2.21 on 2025-06-17 20:08
2+
# FileHash: 8195492c257af2f1cab5ed31d387cef6579a76cde8856ed68def0ce06d55c734
23

34
import ansible_base.feature_flags.models.aap_flag
45
from django.conf import settings
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
### INSTRUCTIONS ###
2+
# If updating the feature_flags.yaml, create a new migration file by copying this one.
3+
# 1. Name the file XXXX_manual_YYYYMMDD.py. For example 0002_manual_20250808.py
4+
# 1. Uncomment the migration below, by uncommenting everything below the FileHash
5+
# 2. Update the dependency to point to the last dependency
6+
# 3. Set the FileHash
7+
###
8+
9+
# FileHash: <FileHash>
10+
11+
# from django.db import migrations
12+
13+
14+
# class Migration(migrations.Migration):
15+
16+
# dependencies = [
17+
# ('dab_feature_flags', '0001_initial'),
18+
# ]
19+
20+
# operations = [
21+
# ]

ansible_base/feature_flags/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def create_initial_data(**kwargs): # NOSONAR
2828
"""
2929
Loads in platform feature flags when the server starts
3030
"""
31-
delete_feature_flags()
31+
purge_feature_flags()
3232
load_feature_flags()
3333

3434

@@ -76,9 +76,9 @@ def load_feature_flags():
7676
logger.error(error_msg)
7777

7878

79-
def delete_feature_flags():
79+
def purge_feature_flags():
8080
"""
81-
If a feature flag has been removed from the platform flags list, delete it from the database.
81+
If a feature flag has been removed from the platform flags list, purge it from the database.
8282
"""
8383
from ansible_base.resource_registry.signals.handlers import no_reverse_sync
8484

ansible_base/feature_flags/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
class FeatureFlagsStatesView(AnsibleBaseDjangoAppApiView, ModelViewSet):
1616
"""
17-
A view class for displaying feature flags states
17+
A view class for displaying feature flags states.
18+
To add/update/remove a feature flag, see the instructions in
19+
`docs/apps/feature_flags.md`
1820
"""
1921

2022
queryset = AAPFlag.objects.order_by('id')

ansible_base/resource_registry/shared_types.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ class TeamType(SharedResourceTypeSerializer):
7878

7979

8080
class FeatureFlagType(SharedResourceTypeSerializer):
81-
"""Serialize list of feature flags"""
82-
8381
RESOURCE_TYPE = "aapflag"
8482
UNIQUE_FIELDS = (
8583
"name",

docs/apps/feature_flags.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ INSTALLED_APPS = [
1818
## Detail
1919

2020
By adding the `ansible_base.feature_flags` app to your application, all Ansible Automation Platform feature flags will be loaded and available in your component.
21-
To receive flag state updates, ensure the following definition is available in your components `RESOURCE_LIST` -
21+
To receive flag state updates, ensure the following definition is available in your components `RESOURCE_LIST` -
2222

2323
```python
2424
from ansible_base.feature_flags.models import AAPFlag
@@ -48,9 +48,9 @@ urlpatterns = [
4848
]
4949
```
5050

51-
## Adding Feature Flags
51+
## Adding/updating/removing feature flags
5252

53-
To add a feature flag to the platform, specify it in the following [file](../../ansible_base/feature_flags/definitions/feature_flags.yaml)
53+
To add/update/remove a feature flag to the platform, ensure its configuration is specified correctly it in the following [file](../../ansible_base/feature_flags/definitions/feature_flags.yaml)
5454

5555
An example flag could resemble -
5656

@@ -73,3 +73,11 @@ Validate this file against the json schema by running `check-jsonschema` -
7373
pip install check-jsonschema
7474
check-jsonschema --schemafile ansible_base/feature_flags/definitions/schema.json ansible_base/feature_flags/definitions/feature_flags.yaml
7575
```
76+
77+
After adding/updating/removing a feature flag, make a manual migration. This can be done by -
78+
79+
1. Copying this [example-migration](../../ansible_base/feature_flags/migrations/example_migration).
80+
2. Name the file XXXX_manual_YYYYMMDD.py. For example 0002_manual_20250808.py
81+
3. Uncomment the migration, by uncommenting everything below the FileHash
82+
4. Update the dependency in the migration to point to the previous migration
83+
5. Set the **FileHash** in the migration file

test_app/tests/feature_flags/migrations/__init__.py

Whitespace-only changes.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import hashlib
2+
import os
3+
4+
from django.conf import settings
5+
from django.test import TestCase
6+
7+
8+
class FileHashTest(TestCase):
9+
FILE_TO_CHECK_PATH = os.path.join(settings.BASE_DIR, 'ansible_base', 'feature_flags', 'definitions', 'feature_flags.yaml')
10+
HASH_ALGORITHM = 'sha256'
11+
HASH_COMMENT_PREFIX = '# FileHash:'
12+
13+
def _get_last_migration_file(self):
14+
"""
15+
Finds the path to the last migration file in the specified Django app.
16+
"""
17+
migrations_dir = os.path.join(settings.BASE_DIR, 'ansible_base', 'feature_flags', 'migrations')
18+
if not os.path.isdir(migrations_dir):
19+
raise FileNotFoundError(f"Migrations directory not found for app: {self.APP_NAME}")
20+
21+
migration_files = sorted([
22+
f for f in os.listdir(migrations_dir)
23+
if f.endswith('.py') and f != '__init__.py'
24+
])
25+
26+
if not migration_files:
27+
raise FileNotFoundError(f"No migration files found in {migrations_dir}")
28+
29+
return os.path.join(migrations_dir, migration_files[-1])
30+
31+
def _extract_hash_from_migration(self, migration_file_path):
32+
"""
33+
Extracts the expected hash from a comment in the migration file.
34+
Assumes the format: '# FileHash: <hash_value>'
35+
"""
36+
with open(migration_file_path, 'r') as f:
37+
for line in f:
38+
if line.strip().startswith(self.HASH_COMMENT_PREFIX):
39+
return line.strip().replace(self.HASH_COMMENT_PREFIX, '').strip()
40+
return None
41+
42+
def _calculate_file_hash(self, file_path):
43+
"""
44+
Calculates the hash of the given file.
45+
"""
46+
hash_func = getattr(hashlib, self.HASH_ALGORITHM, None)
47+
if not hash_func:
48+
raise ValueError(f"Unsupported hash algorithm: {self.HASH_ALGORITHM}")
49+
50+
hasher = hash_func()
51+
with open(file_path, 'rb') as f:
52+
for chunk in iter(lambda: f.read(4096), b""):
53+
hasher.update(chunk)
54+
return hasher.hexdigest()
55+
56+
def test_file_hash_matches_migration_comment(self):
57+
"""
58+
Checks if the hash of a specified file matches the hash commented
59+
in the last migration file.
60+
"""
61+
# 1. Get the last migration file
62+
try:
63+
last_migration_file = self._get_last_migration_file()
64+
except FileNotFoundError as e:
65+
self.fail(f"Could not find last migration file: {e}")
66+
67+
# 2. Extract the expected hash from the migration file
68+
expected_hash = self._extract_hash_from_migration(last_migration_file)
69+
self.assertIsNotNone(expected_hash,
70+
f"No hash comment '{self.HASH_COMMENT_PREFIX}' found in {last_migration_file}")
71+
self.assertTrue(expected_hash, "Extracted hash is empty.")
72+
73+
# 3. Calculate the hash of the target file
74+
self.assertTrue(os.path.exists(self.FILE_TO_CHECK_PATH),
75+
f"File to check does not exist: {self.FILE_TO_CHECK_PATH}")
76+
actual_hash = self._calculate_file_hash(self.FILE_TO_CHECK_PATH)
77+
78+
# 4. Compare the hashes
79+
self.assertEqual(expected_hash, actual_hash,
80+
f"Hash mismatch for '{os.path.basename(self.FILE_TO_CHECK_PATH)}'. "
81+
f"Expected: {expected_hash}, Got: {actual_hash} "
82+
f"If the feature_flags.yaml file changed, generate a new no-op migration file, and correctly set the FileHash.")

0 commit comments

Comments
 (0)